<?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>DEV Community: Justin Schroeder</title>
    <description>The latest articles on DEV Community by Justin Schroeder (@justinschroeder).</description>
    <link>https://dev.to/justinschroeder</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%2F351620%2F1abb4bdb-7e28-4160-abaf-e081a07807dc.png</url>
      <title>DEV Community: Justin Schroeder</title>
      <link>https://dev.to/justinschroeder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/justinschroeder"/>
    <language>en</language>
    <item>
      <title>Shakespeare makes you a better engineer.</title>
      <dc:creator>Justin Schroeder</dc:creator>
      <pubDate>Tue, 16 Sep 2025 22:24:41 +0000</pubDate>
      <link>https://dev.to/justinschroeder/shakespeare-makes-you-a-better-engineer-4919</link>
      <guid>https://dev.to/justinschroeder/shakespeare-makes-you-a-better-engineer-4919</guid>
      <description>&lt;p&gt;I made a benchmark for "discernment" several months ago — how well can a model judge. It has been the most accurate predictor of the utility of a model in agents and coding that I have seen to date.&lt;/p&gt;

&lt;p&gt;It turns out that discernment is incredibly important when writing software, and models with poor discernment may be good at the syntax but they are terrible at the broader task of engineering.&lt;/p&gt;

&lt;p&gt;See, the role of software engineer, when done properly, is not primarily about pounding out syntax. Being good at typing and syntax is just a prerequisite to do the real job: pathfinding in an infinite problem space. The primary skill that all good senior engineers have is an above average ability to make subjective judgment calls at each step of the problem solving process. Given two or three iterations, a good senior engineer's solution approaches the optimal path. Not that dissimilar from a great race car driver finding the optimal racing lines on a given track.&lt;/p&gt;

&lt;p&gt;To make coding models faster, more token efficient, and cheaper to operate — they often are trained, and reinforced, with code, code and more code — and if we were training a race car driver, this would work great. However, unlike the racetrack, the solution space of coding is infinitely vast — discerning a path through it efficiently leverages everything we know, everything we are.&lt;/p&gt;

&lt;p&gt;In other words: I can't explain why organized sports, reciting Shakespeare, memorizing scripture, or playing the piano makes you a better engineer, but it does. Along with brute experience, it is those seemingly unrelated disciplines that shape and craft your ability to make judgment calls. To discern.&lt;/p&gt;

&lt;p&gt;The same is true for models. Stripped down and efficient models can be excellent at various discrete tasks, but finding an elegant solution to a hard problem? Nah, turns out you need to know how to change a diaper at 3am to do that well.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
      <category>career</category>
    </item>
    <item>
      <title>Building Bod.Coach: LLM Lessons Learned The Hard Way.</title>
      <dc:creator>Justin Schroeder</dc:creator>
      <pubDate>Tue, 22 Jul 2025 19:30:41 +0000</pubDate>
      <link>https://dev.to/justinschroeder/building-bodcoach-llm-lessons-learned-the-hard-way-59kf</link>
      <guid>https://dev.to/justinschroeder/building-bodcoach-llm-lessons-learned-the-hard-way-59kf</guid>
      <description>&lt;p&gt;I don't like working out. I do it, but I don't like it. In fact, I was doing the same gym routine for three years without touching it because I didn't want to spend the time researching and changing the exercises I was doing to meet my distant fitness goals (Greek god—without the effort, obviously).&lt;/p&gt;

&lt;p&gt;Like any self-respecting developer, I turned to AI. Surely these models could crank out top-notch workout routines for me and I could just pop them into a database and &lt;em&gt;boom&lt;/em&gt;—problem solved.&lt;/p&gt;

&lt;p&gt;You silly, naïve little man. Not all that glitters is gold.&lt;/p&gt;

&lt;p&gt;In the end, we successfully created &lt;a href="https://bod.coach" rel="noopener noreferrer"&gt;Bod.Coach&lt;/a&gt;—a truly unique fitness experience where you are paired with a virtual fitness trainer via text message. Your trainer has an actual phone number, and you carry on a conversation with it to create your routine, schedule your workouts, and give feedback. You can text any time to change your schedule or let it know you got a shoulder injury—it'll reschedule workouts, change exercises to accommodate your injury, or make you one-off hotel room workouts when you travel. It will track your progress and change workouts dynamically over time to meet your goals. It'll text workout reminders too and hold you accountable by scolding you when you're not living up to your own goals.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fptfuqvolayemzquphqm1.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fptfuqvolayemzquphqm1.gif" alt="Screenshot of interaction" width="720" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But creating &lt;a href="https://bod.coach" rel="noopener noreferrer"&gt;Bod.Coach&lt;/a&gt; turned out to be far more difficult than we anticipated and working with LLMs was by far the hardest part. &lt;/p&gt;

&lt;p&gt;Have you tried actually building an app with AI at the core? It's one of the greatest paradoxes I've encountered in 20+ years of writing software. It's dead simple to wire up a fully functional demo but so &lt;em&gt;so&lt;/em&gt; hard to make it reliable and good. Why? Because your intuition—that problem-solving muscle memory you've built up over your career as a developer—is absolutely worthless.&lt;/p&gt;

&lt;p&gt;The good news is you &lt;em&gt;can&lt;/em&gt; build an intuition for LLMs, but just like becoming a competent software engineer there is no shortcut. You have to build up reps working with them (and I don't mean using ChatGPT).&lt;/p&gt;

&lt;p&gt;My first surprise was asking GPT-4o (via the web interface) for an upper-body arm workout given my particular goals and personal stats—it spit out a pretty great workout! So I went to the OpenAI platform console and crafted a tool call in their interface to capture the important details of a workout as structured data. I asked the same questions, and the result was absolute garbage. It had me doing five reps of barbell curls and then child's pose for 30 minutes.&lt;/p&gt;

&lt;p&gt;This is when I began the journey of learning lesson number one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1: LLMs have a left and right brain.
&lt;/h2&gt;

&lt;p&gt;LLMs are mostly right-brained. They feel more than they think. When we tested LLMs, we found the output format had almost as much to do with determining the quality of the response as the prompt itself. Let's use cookies as an example...&lt;/p&gt;

&lt;p&gt;If you ask a human to write a chocolate-chip-cookie dough recipe on a piece of cardstock and then ask them to write one in a spreadsheet, you get roughly the same result. &lt;strong&gt;This is not true with LLMs.&lt;/strong&gt; The quality of the recipe will be lower when requested as a structured output. This is especially true for non-thinking models.&lt;/p&gt;

&lt;p&gt;When output is requested in a formatted structure—like a tool call—it feels as though it uses a different "part of the brain." My anthropomorphized narrative is that it's thinking more about the syntax than the content.&lt;/p&gt;

&lt;p&gt;In reality LLMs are masters of linguistic form. They don't actually know what a good cookie-dough recipe is—they're just great at predicting the language structure that represents a good recipe. Ask for that as JSON and it suddenly becomes very unclear to its silicon brain what "good" looks like. This structured part of its brain is excellent at producing valid JSON, but not at doing both tasks at the same time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution?
&lt;/h3&gt;

&lt;p&gt;Thinking models help a bit, but in our experience there is still no solution better than &lt;strong&gt;chaining&lt;/strong&gt;. First, ask an LLM to write out that cookie-dough recipe in exquisite detail, giving it no restrictions on output format. That text becomes the input to another LLM request that structures the recipe into JSON (using tools).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: LLMs are always dying
&lt;/h2&gt;

&lt;p&gt;Do you remember the cloning machine Nikola Tesla built in &lt;em&gt;The Prestige&lt;/em&gt; (the most underrated movie of all time)? The antagonist cloned himself every night and killed his current self each night as a magic trick. Perhaps this is my most heavy-handed personification of LLMs, but yes—they die every time they answer you. Each message, from the LLM's perspective, is the last time it will interact with you—and it will do everything it can to be darn sure you're happy with the response.&lt;/p&gt;

&lt;p&gt;Obviously this isn't technically true. No LLMs were harmed in the making of this article, but if you use this mental model as an intuition hack you can predict their least desirable patterns. They are overly verbose, always trying to cover every side of an argument. They'll be quick to end a conversation that just started. They'll call your agent-termination tools far too early. They won't plan unless you ask them to. There are so many slight tendencies and inclinations that it's worth adopting this frame of mind: it thinks this is the last chance it has to speak to you.&lt;/p&gt;

&lt;p&gt;The onboarding flow for &lt;a href="https://bod.coach" rel="noopener noreferrer"&gt;Bod.Coach&lt;/a&gt; is unique. Your fitness trainer reaches out via text message and starts asking questions like "What are your fitness goals?" and "Are you working out at a gym or do you prefer home workouts?" It turns out this was a surprisingly hard problem.&lt;/p&gt;

&lt;p&gt;Initially we told our trainers to carry on a conversation to collect all the information they deemed necessary to create a workout plan to meet the user's fitness goals. When enough information was collected, it could optionally call a tool like &lt;code&gt;onboarding_complete&lt;/code&gt;. This led to roughly 20% of requests following this pattern:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe93nlqjn7i8am29j47dh.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe93nlqjn7i8am29j47dh.png" alt="Early termination" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The LLM was constantly calling our onboarding-complete tool prematurely. To correct this, we gave the LLM a series of things that most human fitness trainers would want to know before creating a workout program. But of course, it was then over-eager to follow these even when they didn't make sense:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F73t4jn60kkwhqb7pd75l.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F73t4jn60kkwhqb7pd75l.png" alt="Over eager" width="800" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution?
&lt;/h3&gt;

&lt;p&gt;After many hilarious (and frustrating) iterations we landed on something that works: we explicitly tell the LLM how many more times it can ask a question and to ask only the most important next one. In our reflection flow, another LLM predicts how many more questions are needed to complete onboarding. This calms those existential fears while removing the need to script the entire flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 3: LLMs have analysis paralysis
&lt;/h2&gt;

&lt;p&gt;It's wing sauce for me—put me in front of an aisle of wing sauce and I'll be there a while. I'll pick one and regret my choice all the way home. Give an LLM a lot of choices and it'll fail like a Roomba in Legoland. The more tools you give an LLM, the worse its decision-making gets.&lt;/p&gt;

&lt;p&gt;Given a user prompt like "Is it going to rain today?" and tools like:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pour_beer  
get_weather  
plan_workout  
call_mom  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;It'll do great—run it 100 times and it'll pick &lt;code&gt;get_weather&lt;/code&gt; almost every time. Add ten unrelated tools and a little extra context and it will start making ridiculous calls. When it goes wrong, it typically goes very wrong. — your customer's will notice.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: "Hey, do you think it will rain today?"
LLM: calling tool call_mom, "Calling mom to ask about the weather"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We did some benchmarking and sure enough, there is an inverse correlation between accuracy and number of tool calls. Here is our super scientific report I showed the 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxp2aghkhcw92u9czg1fb.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxp2aghkhcw92u9czg1fb.png" alt="Chart showing correlation between tools and accuracy" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution?
&lt;/h3&gt;

&lt;p&gt;Break the problem down into a simple decision tree—flowchart style. We call these interstitial LLM prompts &lt;em&gt;routers&lt;/em&gt;. Our internal rule is no more than six tools per LLM request (this may change), so with two levels of nesting you can accommodate 6² = 36 tools—assuming they’re evenly distributed in problem space. In &lt;a href="https://bod.coach" rel="noopener noreferrer"&gt;Bod.Coach&lt;/a&gt;, for example, our router prompts decide which “expert” gets the request:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;send_to_workout_expert  
send_to_account_expert  
send_to_conversation_expert  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Each expert then has a smaller set of tools. This change dramatically improves reliability when you have lots of tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 4: LLMs are Stupid or Stupid Expensive
&lt;/h2&gt;

&lt;p&gt;Developers love hyping how smart models are and how fast they're improving. What’s &lt;em&gt;not&lt;/em&gt; improving quickly is cost and latency. That’s one of the biggest barriers to consumer AI apps. They just aren’t economically feasible. Sure, in a scorched-earth “get users at all costs” era it's fine to run a deficit for a while, but most businesses can't. I pay $200/month for Claude Max at work and don't bat an eye, but I'd rather sell a kidney than pay $50/month for a personal subscription.&lt;/p&gt;

&lt;p&gt;We could make &lt;a href="https://bod.coach" rel="noopener noreferrer"&gt;Bod.Coach&lt;/a&gt; incredible by letting every single text, workout, and scheduling request be handled by Anthropic’s Opus, o3, or Groq-4—but the subscription price would be astronomical. Instead we offer it for $11.99/month (billed annually). How?&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd45b7ou7fz4vx8tfj8k6.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd45b7ou7fz4vx8tfj8k6.png" alt="Cost of AI is either low or too high" width="800" height="575"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution?
&lt;/h3&gt;

&lt;p&gt;Routers again. Because routers categorize prompts into discrete, deterministic domains, we can scale model power—and cost—by task complexity. On &lt;a href="https://bod.coach" rel="noopener noreferrer"&gt;Bod.Coach&lt;/a&gt; your workouts are planned by a bleeding-edge model—we always want top-notch programs. But when you're just chatting about the rain, a cheaper model handles it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 5: Build Your Own LLM Tooling
&lt;/h2&gt;

&lt;p&gt;Off-the-shelf tools can save time, but no one has invented the perfect wheel yet. Capabilities, techniques, APIs, and models change so quickly that locking into any vendor feels risky. Building your own tooling has never been simpler.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Log everything&lt;/strong&gt;: exact LLM requests, conversation history, model used, tools provided, and the response.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider shim&lt;/strong&gt;: support the native APIs of multiple model providers; third-party shims often miss provider-specific features.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault tolerance&lt;/strong&gt;: LLMs fail often—implement retries and instant failover to an equivalent model elsewhere.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flow control&lt;/strong&gt;: chain prompts so outputs feed directly into the next prompt.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent control&lt;/strong&gt;: boot, log, monitor, and, if needed, kill agents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no quick fix here. Just start building. Try tools if they help, but ultimately I recommend owning this part of the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We're really proud of &lt;a href="https://bod.coach" rel="noopener noreferrer"&gt;Bod.Coach&lt;/a&gt; (you should try it—there's a free trial!). It has proven to &lt;em&gt;actually&lt;/em&gt; help people work out and stay more consistent than they otherwise would. Yet the path to success was littered with dead ends, wrong assumptions, and bizarre solutions. Your intuition is wrong, and there are no manuals for success—but, as always, the path less traveled is the most rewarding.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bod.coach" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7s4qd3qmjy6zgm6p6jb5.jpg" alt=" " width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Introducing: KickStart — a form builder for developers.</title>
      <dc:creator>Justin Schroeder</dc:creator>
      <pubDate>Thu, 15 Aug 2024 20:52:21 +0000</pubDate>
      <link>https://dev.to/justinschroeder/introducing-kickstart-a-form-builder-for-developers-7k9</link>
      <guid>https://dev.to/justinschroeder/introducing-kickstart-a-form-builder-for-developers-7k9</guid>
      <description>&lt;p&gt;TLDR: &lt;a href="https://kickstart.formkit.com" rel="noopener noreferrer"&gt;kickstart.formkit.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're reading this you're a developer. Which means you (unfortunately) spend a serious chunk of your time creating forms. Sometimes those forms are small and simple like login forms, sometimes they're complex Rube Goldberg machines, but they are almost always eye-wateringly tedious.&lt;/p&gt;

&lt;p&gt;All those labels, help text, validation rules, styles, internationalization — for every single input — it's boring and soul-crushing work. Take this paper form as an example:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ci6p4y8i69m3fwwhcmw.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ci6p4y8i69m3fwwhcmw.png" alt="Mayo Clinic Patient Intake Form" width="800" height="926"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We're all familiar with filling out forms like this ☝️ (this particular one is an actual patient registration form for the Mayo Clinic hospital system), but imagine if your boss asked you to add that to your web app. Do you know how dense that is? No fewer than 70 inputs! 70!!&lt;/p&gt;

&lt;p&gt;Enter KickStart — a next-generation form builder specifically for developers. KickStart generates forms from a screenshot, SQL query, CSV, markdown file, or a simple prompt. That paper form becomes actual working and running inputs in seconds. You can then edit the form to your heart's content using manual tools, or a conversational chat interface.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz3njbsicgn6q1skcimcg.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz3njbsicgn6q1skcimcg.gif" alt="Teaser showing KickStart generating some forms" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each iteration of your form is saved — so if the AI messes up (or you do 😉) you can easily revert to any previous state.&lt;/p&gt;

&lt;p&gt;The best part is that when you've got it in a good spot — just copy and paste the resulting Vue components into your application. This means you, developer friend, still have all the power to fine-tune your form using real code — not some half-baked pseudo programming flow tool.&lt;/p&gt;

&lt;p&gt;At FormKit, we're developers just like you. We built KickStart because we were sick of the mind-numbing tedium of form creation. It's the tool we needed, and now it's here for all of us. Give it a shot – we think you'll dig it as much as we do.&lt;/p&gt;

&lt;p&gt;Try it out: &lt;a href="https://kickstart.formkit.com" rel="noopener noreferrer"&gt;kickstart.formkit.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>vue</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
    <item>
      <title>Rolldown: why it matters</title>
      <dc:creator>Justin Schroeder</dc:creator>
      <pubDate>Fri, 08 Mar 2024 15:18:17 +0000</pubDate>
      <link>https://dev.to/justinschroeder/rolldown-why-it-matters-4129</link>
      <guid>https://dev.to/justinschroeder/rolldown-why-it-matters-4129</guid>
      <description>&lt;p&gt;Rolldown (just open-sourced) might be the most important web project of the next 5-10 years. I thought it might be helpful to explain why:&lt;/p&gt;

&lt;h2&gt;
  
  
  The landscape:
&lt;/h2&gt;

&lt;p&gt;Rollup is the best and most comprehensive bundler to date. It was ahead of its time, pioneering key concepts like tree shaking (🙏 &lt;a href="https://twitter.com/Rich_Harris" rel="noopener noreferrer"&gt;@Rich_Harris&lt;/a&gt; &amp;amp; &lt;a href="https://twitter.com/lukastaegert" rel="noopener noreferrer"&gt;@lukastaegert&lt;/a&gt;) and still hasn’t been matched in this regard. When &lt;a href="https://twitter.com/youyuxi" rel="noopener noreferrer"&gt;@youyuxi&lt;/a&gt; introduced &lt;a href="https://twitter.com/vite_js" rel="noopener noreferrer"&gt;@vite_js&lt;/a&gt; — Rollup was a cornerstone of the build process. Vite plugins are (for the most part) Rollup plugins.&lt;/p&gt;

&lt;p&gt;It didn’t take long for Vite to become the standard development and build environment for nearly every major meta framework: &lt;a href="https://twitter.com/nuxt_js" rel="noopener noreferrer"&gt;@nuxt_js&lt;/a&gt;, &lt;a href="https://twitter.com/solid_js" rel="noopener noreferrer"&gt;@solid_js&lt;/a&gt; start, &lt;a href="https://twitter.com/sveltejs" rel="noopener noreferrer"&gt;@sveltejs&lt;/a&gt; kit, &lt;a href="https://twitter.com/remix_run" rel="noopener noreferrer"&gt;@remix_run&lt;/a&gt;, &lt;a href="https://twitter.com/angular" rel="noopener noreferrer"&gt;@angular&lt;/a&gt; cli, etc. (basically everything except Next). Rollup is still the underpinning of all of these. It has gone from being a way to bundle your library before publishing on npm to the backbone of how the modern web is being written.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem:
&lt;/h2&gt;

&lt;p&gt;We find ourselves in a world where Rollup has been pressed into service building nearly every byte on the web — something it wasn’t originally intended to do — and its performance (really JavaScript’s performance) could be improved. esbuild (🙏 &lt;a href="https://twitter.com/evanwallace" rel="noopener noreferrer"&gt;@evanwallace&lt;/a&gt;), a bundler written in Go, showed the world just how much faster a "native" bundler can be, but esbuild was not Rollup compatible and had its fair share of shortcomings as well (no TypeScript, limited tree shaking, etc.).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/vite_js" rel="noopener noreferrer"&gt;@vite_js&lt;/a&gt; uses esbuild and Rollup to get the best of both worlds, but here lies another problem: parsing. Different build tools use different AST trees, and each of these must parse the code to derive the AST. Also, many plugins for Rollup/Vite perform their own parsing. Wouldn’t it be incredible if the parsing was done a single time and a single AST tree could be used by all layers of the build stack?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/youyuxi" rel="noopener noreferrer"&gt;@youyuxi&lt;/a&gt; said recently that &lt;a href="https://twitter.com/vite_js" rel="noopener noreferrer"&gt;@vite_js&lt;/a&gt; often has to parse the same TS file at least 5 times at various levels in the stack. Clearly this could be better.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution:
&lt;/h2&gt;

&lt;p&gt;The Rolldown project is attempting to (eventually) solve all of these problems. First, it is written in Rust, one of the fastest languages out there — generally even faster than Go (i.e., esbuild) due to Rust not using garbage collection. In some applications, this doesn’t matter much, but in the context of parsing and compiling, this is a big benefit (oh the irony that Rust itself is the world’s slowest compiler 😂). Plus, people like &lt;a href="https://twitter.com/ThePrimeagen" rel="noopener noreferrer"&gt;@ThePrimeagen&lt;/a&gt; talk about Rust — somehow that makes it fast by proxy.&lt;/p&gt;

&lt;p&gt;Rolldown isn’t just attempting to be fast though, it is also attempting to be API compatible with Rollup. This would be a monumental accomplishment (the Rollup API is not for the faint of heart). If successful, the &lt;a href="https://twitter.com/vite_js" rel="noopener noreferrer"&gt;@vite_js&lt;/a&gt; team could swap out the engine inside Vite while the plane is flying.&lt;/p&gt;

&lt;p&gt;Finally — and perhaps most ambitiously — a standard singular parse. Perhaps this is a bit further down the road, but because Vite is so prevalent and because Rolldown uses OXC (🙏 &lt;a href="https://twitter.com/boshen_c" rel="noopener noreferrer"&gt;@boshen_c&lt;/a&gt;) for parsing, we could end up in a world that standardizes around a single parse and AST tree. The performance improvements we could see in our development environment and build times would be tremendous.&lt;/p&gt;

&lt;p&gt;If successful, Rolldown will be responsible for transforming nearly every byte of web code — no matter what framework you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thanks:
&lt;/h2&gt;

&lt;p&gt;Finally, thanks to the &lt;a href="https://twitter.com/vite_js" rel="noopener noreferrer"&gt;@vite_js&lt;/a&gt; team, and everyone working on &lt;a href="https://twitter.com/rolldown" rel="noopener noreferrer"&gt;@rolldown&lt;/a&gt; (&lt;a href="https://twitter.com/_hyf0" rel="noopener noreferrer"&gt;@_hyf0&lt;/a&gt;, &lt;a href="https://twitter.com/_h_ana___" rel="noopener noreferrer"&gt;@_h_ana___&lt;/a&gt;, &lt;a href="https://twitter.com/youyuxi" rel="noopener noreferrer"&gt;@youyuxi&lt;/a&gt;)! I am in no way affiliated with these fine engineers, just a mere plebe building on the backs of these giants.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Re-posted from here:&lt;br&gt;
&lt;iframe class="tweet-embed" id="tweet-1766115866859446477-906" src="https://platform.twitter.com/embed/Tweet.html?id=1766115866859446477"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1766115866859446477-906');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1766115866859446477&amp;amp;theme=dark"
  }



&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>rust</category>
    </item>
    <item>
      <title>Introducing Tempo • A new date library for JavaScript (and TypeScript).</title>
      <dc:creator>Justin Schroeder</dc:creator>
      <pubDate>Wed, 14 Feb 2024 10:00:00 +0000</pubDate>
      <link>https://dev.to/justinschroeder/introducing-tempo-a-new-date-library-for-javascript-and-typescript-359a</link>
      <guid>https://dev.to/justinschroeder/introducing-tempo-a-new-date-library-for-javascript-and-typescript-359a</guid>
      <description>&lt;p&gt;TLDR; &lt;a href="https://tempo.formkit.com" rel="noopener noreferrer"&gt;Checkout the Tempo docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Working with dates and time is one of JavaScript’s weakest points. The native Date object is, under the hood, just a unix timestamp with some utility methods.&lt;/p&gt;

&lt;p&gt;Fortunately, there is no shortage of libraries like moment.js, luxon, date-fns, and day.js to fill the gap. Which brings up the question you’ve already asked yourself, — why another date library?&lt;/p&gt;

&lt;p&gt;Of course I (&lt;a href="https://x.com/jpschroeder" rel="noopener noreferrer"&gt;👋 its me, Justin Schroeder&lt;/a&gt;) didn’t set out to create a whole new library, but the following needs have pushed me over the edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; styles
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; API is an incredible underutilized resource. For example, while it is excellent at formatting dates with locale aware format strings like &lt;code&gt;{ dateStyle: "full" }&lt;/code&gt; there is no easy way to reverse this process. &lt;code&gt;Friday, March 14, 1997&lt;/code&gt; is the "full" dateStyle in the locale &lt;code&gt;en-US&lt;/code&gt; but how do you parse it back into a date? With tempo you can:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parse&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;@formkit/tempo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Friday, March 14, 1997&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;full&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;en&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;h3&gt;
  
  
  Parse what you format
&lt;/h3&gt;

&lt;p&gt;Anything you can format with Tempo you can parse with Tempo. For many libraries this is not the case. Bi-directional support allows you to build great locale aware user experiences like the &lt;a href="https://formkit.com/inputs/datepicker" rel="noopener noreferrer"&gt;datepicker in FormKit&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timezones out of the box
&lt;/h3&gt;

&lt;p&gt;Working with dates in real-world applications requires timezone support. Tempo does not require a plugin or additional package but instead mines the &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; for you. Need to know what the time difference between Amsterdam and Kolkata is in 2 months?&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;addMonth&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;@formkit/tempo&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;inTwoMonths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addMonth&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;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inTwoMonths&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Europe/Amsterdam&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;Asia/Kolkata&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;h3&gt;
  
  
  Death to the builder pattern
&lt;/h3&gt;

&lt;p&gt;In almost all cases we &lt;code&gt;should().not().use().theBuilder().pattern()&lt;/code&gt;. The builder pattern cannot be tree shaken and offers no substantial benefits over a more functional style (this is not a rebuke of objects, just this pattern). Unfortunately tools like Luxon and DayJS make heavy use of the builder pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stop the plugins
&lt;/h3&gt;

&lt;p&gt;Many of my favorite date libraries promise an itty bitty core with optional plugins to expand the scope of functionality — unfortunately that little core often doesn’t do much. The same benefit is available to anyone using function notation while tree shaking ensures your project is never bigger than it needs to be. In real-world use cases Tempo is generally ~3-5kb min/gzip including robust support, but depending on what you import it can be as small as 80 bytes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better docs
&lt;/h3&gt;

&lt;p&gt;Most of our time interacting with date libraries is reading docs. I want to enjoy that experience. I also want to tinker with things like the format function directly on the page. Tempo’s docs are (I think) great and every example is interactive so you can test your use case before you write a line of code locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not?
&lt;/h3&gt;

&lt;p&gt;Seriously though, this is an open source project and a free gift to the world. If you are perfectly happy with your current solution, keep using it. If you’d like to try something new — give &lt;a href="https://tempo.formkit.com" rel="noopener noreferrer"&gt;Tempo a shot&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Highlights
&lt;/h2&gt;

&lt;p&gt;Tempo is full featured in ways you would expect, with full support for day.js style formatting tokens, date parsing, and date manipulation. However it does have some nifty tricks up its sleeves that are worth pointing out:&lt;/p&gt;

&lt;h3&gt;
  
  
  Convert a &lt;code&gt;dateStyle&lt;/code&gt; to tokens
&lt;/h3&gt;

&lt;p&gt;Tempo allows you to extract the formatting tokens of any dateStyle or timeStyle:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;formatStr&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;@formkit/tempo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;formatStr&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;full&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;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// dddd, MMMM D, YYYY at h:mm:ss A Z&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using this technique you can create incredible user experiences where users are presented editable dates in their own locale. For examples, checkout FormKit’s datepicker where we use this exact feature: &lt;a href="https://formkit.com/inputs/datepicker" rel="noopener noreferrer"&gt;https://formkit.com/inputs/datepicker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tip: For even more detail you can use the &lt;code&gt;parts()&lt;/code&gt; function.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using timezones
&lt;/h3&gt;

&lt;p&gt;Tempo ships with first class support for timezones — it’s even baked into the &lt;code&gt;format()&lt;/code&gt; function.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;format&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;@formkit/tempo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// What time is it in LA?&lt;/span&gt;
&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;date&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;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hh:mm a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;America/Los_Angeles&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;You can also easily determine the timezone offset from the user’s location to a fixed place. This is useful when building apps where  time at a target location matters. For example booking a rental or a meeting in at a given location from another timezone.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tzDate&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;@formkit/tempo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Create the date for 1:44pm in Dubai.&lt;/span&gt;
&lt;span class="nf"&gt;tzDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2025-02-28 13:44&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;Asia/Dubai&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;h3&gt;
  
  
  Token ranges
&lt;/h3&gt;

&lt;p&gt;Tempo can "mine" information from &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt;. For example, if you want to create a select list of months and display them to a user in their own locale, how can you get all those strings?&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;range&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;@formkit/tempo&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;monthNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MMMM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// January, February, March...&lt;/span&gt;
&lt;span class="c1"&gt;// or in french:&lt;/span&gt;
&lt;span class="c1"&gt;// janvier,février,mars..&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Get building
&lt;/h2&gt;

&lt;p&gt;There are many more features to explore in Tempo — give &lt;a href="https://tempo.formkit.com" rel="noopener noreferrer"&gt;the docs&lt;/a&gt; a quick scan so you know what’s available the next time you’re working on a project that requires working with dates.&lt;/p&gt;

&lt;p&gt;While I have you here — can I ask you a personal favor? Consider giving &lt;a href="https://github.com/formkit/tempo/stargazers" rel="noopener noreferrer"&gt;Tempo a star on GitHub&lt;/a&gt;...it makes me happy.&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;really&lt;/em&gt; like Tempo, consider &lt;a href="https://github.com/sponsors/formkit" rel="noopener noreferrer"&gt;sponsoring FormKit&lt;/a&gt;! We don’t make a dime off this open source work, so any sponsorships we get are hugely encouraging.&lt;/p&gt;

&lt;p&gt;❤️&lt;/p&gt;

&lt;p&gt;See announcement tweet 👇&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1757462744595300822-982" src="https://platform.twitter.com/embed/Tweet.html?id=1757462744595300822"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1757462744595300822-982');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1757462744595300822&amp;amp;theme=dark"
  }



&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>node</category>
    </item>
    <item>
      <title>I’m a software engineer, what does AI mean for me?</title>
      <dc:creator>Justin Schroeder</dc:creator>
      <pubDate>Wed, 22 Mar 2023 19:41:15 +0000</pubDate>
      <link>https://dev.to/justinschroeder/im-a-software-engineer-what-does-ai-mean-for-me-1e8j</link>
      <guid>https://dev.to/justinschroeder/im-a-software-engineer-what-does-ai-mean-for-me-1e8j</guid>
      <description>&lt;h2&gt;
  
  
  The ticking clock
&lt;/h2&gt;

&lt;p&gt;Seven years ago, a co-worker and I were walking to lunch in our small town of Charlottesville, Virginia. To our surprise, we realized we had passed &lt;em&gt;three&lt;/em&gt; digital agencies on our short stroll. Web developers are everywhere — and for good reason. It’s nearly impossible to run a business without a digital presence. The world needs our “product.”&lt;/p&gt;

&lt;p&gt;Yet at that moment, I remember saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Too many people are getting paid too much for this industry to go on without disruption. The target on our backs is too big.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That moment, seven years ago, was the beginning of a career change for me. I realized that the clock was ticking on my toolset being “special.” It wasn’t long after that walk with my co-worker that I left the agency I was working for to take a stab at building a business of my own — before the clock ran out on my abilities being “special.”&lt;/p&gt;

&lt;p&gt;As software engineers, we have long enjoyed being a necessity in nearly every start-up. Famously, many start-up incubators require one of the founders to be an engineer, and often the first big hiring wave of a growing business is... engineers. Meanwhile, developer salaries have ballooned — I know one self-taught engineer making $600k/year. Again, the target on our backs is &lt;em&gt;massive&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The match
&lt;/h2&gt;

&lt;p&gt;I thought the impending engineering disruption would come from increasingly powerful SaaS products like Squarespace, WebFlow, or no-code. The day GitHub co-pilot was released, I realized I was wrong. The disruption would come from AI. I told our team that first day:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I may or may not use co-pilot (of course, I ended up using it), but I really believe this is the beginning of the end of our industry.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My time horizon for that statement was denominated in decades. Perhaps, I thought, in 20 years, the world will require 50% fewer engineers.&lt;/p&gt;

&lt;p&gt;I was off by a factor of ten.&lt;/p&gt;

&lt;h2&gt;
  
  
  The legacy of replacement
&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6eo1xnxp49buihxscs6y.jpg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6eo1xnxp49buihxscs6y.jpg" alt="Window knocker uppers are waking people up during the industrial revolution. Credit https://www.vintag.es" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Jobs have been changing for centuries. Some jobs become obsolete while new jobs are created in their wake. The industrial revolution eliminated scores of jobs but also created many new ones. Factory shift work was born. Suddenly it became essential to get to work on time — so a new job was born, the window “knocker-upper.” This person would simply knock on your window to wake you up in the morning. Eventually, this new role was itself replaced by the alarm clock.&lt;/p&gt;

&lt;p&gt;Before phototypesetting, printing press plates were manually assembled by a “compositor” — a job made entirely obsolete by newer technology (it lives on in our digital jargon). More recently, digital photography caused massive disruption to the film industry. It wasn’t that long ago that every Walmart had a darkroom technician, Kodak was a household name, and polaroids were convenient — not hipster.&lt;/p&gt;

&lt;p&gt;The dawn of digital photography was a warning shot across the bow of the film industry. Adapt or die. Most died. I imagine the scuttlebutt in their hallways sounded a lot like developer Twitter does today, “digital cameras just can’t &lt;strong&gt;x&lt;/strong&gt;” — a desperate and combative search for relevance in the face of unrelenting market efficiency.&lt;/p&gt;

&lt;h2&gt;
  
  
  New roles and old ones?
&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsn0j4x4j4va81h89p14b.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsn0j4x4j4va81h89p14b.png" alt="A farmer doing farm thins" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s not all doom and gloom, though. Prompt engineer, data researcher, cybernetically enhanced humanoid software engineer, farmer — the jobs we have in the future may not yet exist, or they may be a resurgence of jobs as old as time. One thing is certain, though, new jobs &lt;em&gt;will&lt;/em&gt; emerge — the challenge of knowing exactly where is an &lt;a href="https://en.wikipedia.org/wiki/N-body_problem" rel="noopener noreferrer"&gt;n-body&lt;/a&gt; problem — no one can accurately predict it.&lt;/p&gt;

&lt;p&gt;In 2007 when the iPhone was announced, it was impossible to predict that it would directly lead to thousands of people picking up food from local restaurants for delivery, letting strangers sleep in our homes, or causing scores of people to run around New York City trying to catch Pokémon.&lt;/p&gt;

&lt;p&gt;Although AI doomsayers love to tell us how “this time it’s different,” — don’t believe the lie; nothing is new under the sun; it’s always just a different shade of the same thing. &lt;strong&gt;Instead, we should be aware that these changes will come, so don’t let emerging opportunities escape your grasp.  If you cover your ears and close your eyes progress will march on despite your resistance.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How far is that horizon?
&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18rxtlvpjb8ti6fkxpwq.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18rxtlvpjb8ti6fkxpwq.png" alt=" " width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are an engineer reading this, your emotions will fall into one of three categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optimism.&lt;/strong&gt; You’re excited. You see opportunity around every corner, and this is a big corner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Denial.&lt;/strong&gt; You’ve seen ChatGPT make mistakes. It doesn’t even know basic math. Your job is safe. All this hype is just that, hype.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concern.&lt;/strong&gt; You’re not sure what the future holds, but you know things are going to change. You’re grappling to understand how you fit into the unfolding future.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The truth is we don’t know how far we are from a future that causes a dramatic shift in engineering labor. ChatGPT cannot entirely replace a human engineer on any single metric today. It does not — at present — possess the capability for unique critical thinking. However, five years ago, if asked, most engineers would have said their jobs would be one of the last to rest on the chopping block. Now we can all (even the deniers) &lt;em&gt;envision&lt;/em&gt; a future where AI can do our jobs.&lt;/p&gt;

&lt;p&gt;How far are we from that vision becoming a reality? In 2000 BCE Wan Hu imagined going to the moon — he could do it by the end of the day, probably. He strapped 47 rockets to his chair in what was history’s first attempted moon landing. Humanity’s imagination and capability were separated by 4,000 years.&lt;/p&gt;

&lt;p&gt;It is possible that ChatGPT has merely given us the ability to &lt;em&gt;imagine&lt;/em&gt; a world where AIs replace engineers — but the capability to &lt;em&gt;actually&lt;/em&gt; do so might still be a long-distant future.&lt;/p&gt;

&lt;p&gt;Alternatively, we might be window knocker-uppers in the age of alarm clocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What will I do?
&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8r6jf0bypvxihglug94x.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8r6jf0bypvxihglug94x.png" alt=" " width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From a career standpoint — my work still matters. I’m still “better” than the AI — and you are too — but I know I’m going to be keeping an eye out for where things are heading more than I have ever done before. What new tools can be created with this technology? What opportunities are opening up that didn’t previously exist? What new jobs will be created? What am I truly great at? How can I bring unique value into the world? What is my plan if the coming disruption leaves me out?&lt;/p&gt;

&lt;p&gt;How about you? Do you think of yourself as someone who codes because you’ve learned the syntactic shibboleths to get a computer to do what you want? Or are you someone who codes to solve problems? Engineering is solving problems. As AI assistance becomes AI creation, our tools to solve problems will change — if you’re a syntax machine &lt;strong&gt;you will be replaced by a more efficient syntax machine&lt;/strong&gt;. If you are a problem solver, you’ll be a more efficient problem solver — at least for now.&lt;/p&gt;

&lt;p&gt;On a personal level — I’m more inspired than ever to be great at the things AI can never do — be a nurturing father to my kids, a great husband to my wife, and an active member in my community. Life is far too rich and precious to be defined only by the &lt;em&gt;type&lt;/em&gt; of work we do. Live your life, love your people, and for heaven’s sake — eat some good food. After all, the AI can’t do that either — yet.&lt;/p&gt;

</description>
      <category>chatgpt</category>
      <category>ai</category>
      <category>productivity</category>
      <category>career</category>
    </item>
    <item>
      <title>Introducing AutoAnimate — add motion to your apps with a single line of code.</title>
      <dc:creator>Justin Schroeder</dc:creator>
      <pubDate>Thu, 19 May 2022 16:30:33 +0000</pubDate>
      <link>https://dev.to/justinschroeder/introducing-autoanimate-add-motion-to-your-apps-with-a-single-line-of-code-34bp</link>
      <guid>https://dev.to/justinschroeder/introducing-autoanimate-add-motion-to-your-apps-with-a-single-line-of-code-34bp</guid>
      <description>&lt;p&gt;When it comes to building user interfaces, a little motion goes a long way. Whether you are adding a task to your todo list, deleting a contact, or sorting a playlist, interfaces with subtle motion &lt;em&gt;unquestionably&lt;/em&gt; provide a superior user experience. If we know some motion is better — why do we so seldom add it?&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9oog9eisxg95infw45wu.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9oog9eisxg95infw45wu.gif" alt="Motion can be hard to visualize" width="568" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The answer is as simple — &lt;em&gt;it just takes too much time&lt;/em&gt;. Time is money after all, and optimizing for "minor" UX details often falls outside the budget of all but the biggest brands.&lt;/p&gt;

&lt;p&gt;To be fair, adding UI animations can be painful — especially for new, removed, and moved DOM elements. For example, when animating the addition of a new item to a list an experienced dev might do the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Measure the parent element and set its &lt;code&gt;max-height&lt;/code&gt; explicitly, and add a class with css transitions for &lt;code&gt;max-height&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add a class to the new list item before its added to the DOM that sets its initial opacity, and transform state (perhaps a slight scale down).&lt;/li&gt;
&lt;li&gt;Inject the element, and set a very short timeout that removes the initial class.&lt;/li&gt;
&lt;li&gt;Calculate the height of the element being added, and increase the parent’s &lt;code&gt;max-height&lt;/code&gt; by that amount.&lt;/li&gt;
&lt;li&gt;Wait for the height to be fully transition, and remove the explicit max-height properties.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not fun! Enter AutoAnimate.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8w3ctibxsz96ricw10ov.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8w3ctibxsz96ricw10ov.png" alt="AutoAnimation Logo" width="579" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AutoAnimate is a lightweight (1.9Kb), zero-config, drop-in, animation library that automatically applies transition animations to elements being added, removed, or moved in the DOM. It literally takes one line of code to implement, and it works with React, Vue, and any other JavaScript framework you want.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AutoAnimate’s goal is to substantially improve an application’s user-experience without impacting the developer’s implementation time or performance budget.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s take a look at two identical lists written in React — one with AutoAnimate and one without.&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://stackblitz.com/edit/auto-animate-react?ctl=1&amp;amp;embed=1&amp;amp;file=src/App.jsx&amp;amp;hideExplorer=1&amp;amp;hideNavigation=1&amp;amp;theme=dark&amp;amp;view=preview" width="100%" height="500"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The details of the list are just standard React code, but lets take a look at how animations were added to the second list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&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;react&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;FrameworkList&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;./FrameworkList&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;useAutoAnimate&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;@formkit/auto-animate/react&lt;/span&gt;&lt;span class="dl"&gt;'&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;animationParent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAutoAnimate&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"comparison"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FrameworkList&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FrameworkList&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;animationParent&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;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? Yep. And it might even be easier if you use Vue!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;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="nx"&gt;FrameworkList&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;./FrameworkList.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"comparison"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FrameworkList&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FrameworkList&lt;/span&gt; &lt;span class="na"&gt;v-auto-animate&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&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;Of course AutoAnimate works great with plain ol' native JavaScript too! All you need to do is pass the parent DOM element into the autoAnimate 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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#my-el&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;autoAnimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AutoAnimate is made by myself (&lt;a href="https://twitter.com/jpschroeder" rel="noopener noreferrer"&gt;Justin Schroeder&lt;/a&gt;) and the team from &lt;a href="https://formkit.com/" rel="noopener noreferrer"&gt;FormKit&lt;/a&gt;, and as of today the beta is publicly available!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://auto-animate.formkit.com" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgnerj0t6ggczhbbrjfze.png" alt="Examples &amp;amp; Docs" width="215" height="89"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you find AutoAnimate is helping you out, consider supporting us. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/sponsors/formkit" rel="noopener noreferrer"&gt;Sponsor the FormKit organization&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/formkit/auto-animate" rel="noopener noreferrer"&gt;Star the AutoAnimate repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://twitter.com/useformkit" rel="noopener noreferrer"&gt;Follow FormKit&lt;/a&gt; and &lt;a href="https://twitter.com/jpschroeder" rel="noopener noreferrer"&gt;myself&lt;/a&gt; on Twitter.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>vue</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Introducing FormKit: A Vue 3 form building framework</title>
      <dc:creator>Justin Schroeder</dc:creator>
      <pubDate>Wed, 23 Feb 2022 20:16:24 +0000</pubDate>
      <link>https://dev.to/justinschroeder/introducing-formkit-a-vue-3-form-building-framework-53ji</link>
      <guid>https://dev.to/justinschroeder/introducing-formkit-a-vue-3-form-building-framework-53ji</guid>
      <description>&lt;p&gt;Web forms are the connective tissue of the internet. Without them people can’t request their health records, apply for university, order pad thai, or book a plane ticket. Yet too often the tools used to build those forms are a grab bag of npm packages and DIY solutions that result in a subpar user-experience, poor accessibility, and low maintainability.&lt;/p&gt;

&lt;p&gt;In 2020 &lt;a href="https://twitter.com/jpschroeder/status/1236617614618738688?s=20&amp;amp;t=32xC3oOD1p0q_1bm1WY2FA" rel="noopener noreferrer"&gt;I released&lt;/a&gt; a small open source library to help alleviate the constant pain of building forms — it was called &lt;a href="https://vueformulate.com/" rel="noopener noreferrer"&gt;Vue Formulate&lt;/a&gt;. The library was "late to the game" — released for Vue 2 just as Vue 3 was coming on the scene — and developers were already committed to their tools of choice. I had low expectations for adoption &lt;em&gt;and yet developers started using Vue Formulate — a lot&lt;/em&gt;. It seemed others were experiencing the same pain points as I was.&lt;/p&gt;

&lt;p&gt;Let’s review that pain: building high-quality forms is hard. Sure, it’s easy to slap an &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; tag on a page, but taking care to label, group, populate, internationalize and validate every input takes a lot of effort — and all too often we leave critically important features on the alter of time: accessibility, error handling, and an empathetic user experience.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl4wafrwmzt02fc1txny9.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl4wafrwmzt02fc1txny9.png" alt="Poor validation results" width="800" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a user — how often have you submitted a form only to realize there's a validation error somewhere on the page but the application doesn't tell you which input. Or how about those forms where the developer plops a list of validation errors on the top of the page and expects you to sort it out yourself? How often have &lt;em&gt;you&lt;/em&gt; been responsible for doing that 😳? Yeah...me too. Personally, I blame the tooling — enter FormKit.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj8dw1p84x4kyjzoif228.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj8dw1p84x4kyjzoif228.png" alt="The FormKit logo" width="254" height="52"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;FormKit isn’t just the Vue 3 compatible version of Vue Formulate. It isn’t just another validation library, or UI library — it aims much higher: &lt;em&gt;FormKit is a form building &lt;strong&gt;framework&lt;/strong&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can it do?
&lt;/h2&gt;

&lt;p&gt;As of today (early 2022), FormKit has a similar feature set to Vue Formulate but with a new architecture that will allow us to ship exciting features on a regular basis. Let’s hit some highlights:&lt;/p&gt;

&lt;h3&gt;
  
  
  Single component
&lt;/h3&gt;

&lt;p&gt;FormKit is easy to learn — there's only 1 input component &lt;code&gt;&amp;lt;FormKit&amp;gt;&lt;/code&gt;. The convenience of typing &lt;code&gt;&amp;lt;FormKit type="text"&amp;gt;&lt;/code&gt; vs &lt;code&gt;&amp;lt;FormKit type="textarea"&amp;gt;&lt;/code&gt; is more profound than it seems on the surface — it provides a consistent shared API for all team members across all your projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  No more boilerplate
&lt;/h3&gt;

&lt;p&gt;Form inputs require a hefty dose of markup to be implemented correctly. Honestly, sometimes it’s just more annoying than difficult.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdbg53y1f1e95gmbhbl85.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdbg53y1f1e95gmbhbl85.png" alt="Developers voting that they don’t like boilerplate on twitter" width="589" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;FormKit dramatically simplifies boilerplate by providing structured markup out of the box that supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Labels&lt;/li&gt;
&lt;li&gt;Help text&lt;/li&gt;
&lt;li&gt;Validation messages&lt;/li&gt;
&lt;li&gt;Error messages&lt;/li&gt;
&lt;li&gt;Input element&lt;/li&gt;
&lt;li&gt;Wrapper elements
&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;FormKit&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"textarea"&lt;/span&gt;
  &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Message"&lt;/span&gt;
  &lt;span class="na"&gt;help=&lt;/span&gt;&lt;span class="s"&gt;"Enter your message"&lt;/span&gt;
  &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"required|length:50,200"&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;&lt;em&gt;Note: You may have to disable your ad-blocker on this page for the following Stackblitz code examples to run.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://stackblitz.com/edit/intro-example-a?embed=1&amp;amp;file=src/App.vue&amp;amp;hideExplorer=1&amp;amp;hideNavigation=1&amp;amp;view=preview&amp;amp;ctl=1" width="100%" height="500"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Validation
&lt;/h3&gt;

&lt;p&gt;Although there are some great validation libraries for Vue (&lt;a href="https://vee-validate.logaretm.com/v4/" rel="noopener noreferrer"&gt;VeeValidate&lt;/a&gt; and &lt;a href="https://vuelidate-next.netlify.app/" rel="noopener noreferrer"&gt;Vuelidate&lt;/a&gt; to name two), FormKit provides pre-written validation rules which are then declared using the &lt;code&gt;validation&lt;/code&gt; prop. This makes them easier to read and reduces room for developer errors:&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;FormKit&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
  &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
  &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"New password"&lt;/span&gt;
  &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"required|length:6"&lt;/span&gt;
  &lt;span class="na"&gt;help=&lt;/span&gt;&lt;span class="s"&gt;"Enter a new password, at least 6 characters long"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;FormKit&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
  &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password_confirm"&lt;/span&gt;
  &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Password confirmation"&lt;/span&gt;
  &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"required|confirm"&lt;/span&gt;
  &lt;span class="na"&gt;help=&lt;/span&gt;&lt;span class="s"&gt;"Retype your password"&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;&lt;iframe src="https://stackblitz.com/edit/intro-example-a-vzy4ja?embed=1&amp;amp;file=src/components/ExampleForm.vue&amp;amp;hideExplorer=1&amp;amp;hideNavigation=1&amp;amp;view=preview&amp;amp;ctl=1" width="100%" height="500"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Validation errors are automatically displayed on the inputs that are failing — exactly where your users expect them to be. &lt;a href="https://formkit.com/essentials/validation" rel="noopener noreferrer"&gt;There's much more to say about validation&lt;/a&gt;, but that’s why FormKit ships with comprehensive docs 😉.&lt;/p&gt;

&lt;h3&gt;
  
  
  Form state
&lt;/h3&gt;

&lt;p&gt;Are all the inputs in your form valid? Is your form currently loading? Do you need to disable all the inputs at the same time? No problem. FormKit automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adds submit buttons to your forms&lt;/li&gt;
&lt;li&gt;Ensures all inputs pass validation before submitting, and if necessary shows which inputs are still invalid.&lt;/li&gt;
&lt;li&gt;Detects if you’re using an &lt;code&gt;async&lt;/code&gt; submit handler and sets the form state to loading while awaiting a response.&lt;/li&gt;
&lt;li&gt;Disables all inputs in your form while submission is pending.&lt;/li&gt;
&lt;li&gt;Displays a spinner while submission is pending.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple example of this:&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;FormKit&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"form"&lt;/span&gt;
    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;submit=&lt;/span&gt;&lt;span class="s"&gt;"register"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Register&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FormKit&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;label=&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt;
      &lt;span class="na"&gt;help=&lt;/span&gt;&lt;span class="s"&gt;"What is your email address?"&lt;/span&gt;
      &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"required|email"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FormKit&lt;/span&gt;
      &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
      &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Password"&lt;/span&gt;
      &lt;span class="na"&gt;help=&lt;/span&gt;&lt;span class="s"&gt;"Enter your new account’s password"&lt;/span&gt;
      &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"required|length:6"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FormKit&lt;/span&gt;
      &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password_confirm"&lt;/span&gt;
      &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Confirm password"&lt;/span&gt;
      &lt;span class="na"&gt;help=&lt;/span&gt;&lt;span class="s"&gt;"Re-type that previous password"&lt;/span&gt;
      &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"required|confirm"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/FormKit&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;register&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;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;// Let's imagine this is a submit handler&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2000&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;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;iframe src="https://stackblitz.com/edit/intro-example-a-lg4wbr?embed=1&amp;amp;file=src/components/ExampleForm.vue&amp;amp;hideExplorer=1&amp;amp;hideNavigation=1&amp;amp;view=preview&amp;amp;ctl=1" width="100%" height="500"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Model binding
&lt;/h3&gt;

&lt;p&gt;FormKit always knows the precise value of your form and all of its inputs — no matter how large. In fact, you can even &lt;code&gt;v-model&lt;/code&gt; an entire form with 2-way data binding — this makes repopulating a forms a breeze.&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;FormKit&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"form"&lt;/span&gt;
    &lt;span class="na"&gt;submit-label=&lt;/span&gt;&lt;span class="s"&gt;"Update"&lt;/span&gt;
    &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"values"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Edit account&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FormKit&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;"first"&lt;/span&gt;
      &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"First name"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FormKit&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;"last"&lt;/span&gt;
      &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Last name"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FormKit&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;label=&lt;/span&gt;&lt;span class="s"&gt;"Email address"&lt;/span&gt;
      &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"required|email"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/FormKit&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;"randomEmail"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Randomize email&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;pre&amp;gt;&lt;/span&gt;{{ values }}&lt;span class="nt"&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&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="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;values&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;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Katja&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Marabello&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="s1"&gt;katja@gmail.com&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;randomEmail&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;randomString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&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="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;randomString&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@example.com`&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;&lt;iframe src="https://stackblitz.com/edit/intro-example-a-yb1xbj?embed=1&amp;amp;file=src/components/ExampleForm.vue&amp;amp;hideExplorer=1&amp;amp;hideNavigation=1&amp;amp;view=preview&amp;amp;ctl=1" width="100%" height="500"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Plugins
&lt;/h3&gt;

&lt;p&gt;Plugins in FormKit are...powerful. In fact, many of the "advertised" core features, like internationalization (i18n), validation, Vue support, and even the inputs themselves are actually just first-party plugins. That said, the architecture of a FormKit plugin is unique.&lt;/p&gt;

&lt;p&gt;Instead of having a centralized instance — like a "FormKit global object" — each input is its own mini-application, complete with its own plugins, store, and children.&lt;/p&gt;

&lt;p&gt;Practically speaking, this means that you can expose a completely different set of features to one form on your site vs another — and you can code split and tree-shake those features to give you even better performance for your project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schema
&lt;/h3&gt;

&lt;p&gt;Ahh, finally we come to FormKit’s schema. This is the most requested feature upgrade from Vue Formulate. The FormKit schema is a JSON-serializable data format that can describe virtually any HTML or Vue template structure.&lt;/p&gt;

&lt;p&gt;To power this feature, we wrote a small runtime compiler that is able to make quick work of any HTML elements, Vue components, conditional expressions (if/then/else statements), mathematical expressions, boolean logic, loops, slots, and data references. It is so powerful, in fact, that we wrote all of our existing FormKit inputs using it.&lt;/p&gt;

&lt;p&gt;Now you can store your forms anywhere you can store JSON — like a database 😉. There is so much more to say about the schema, but again, that’s why &lt;a href="https://formkit.com/advanced/schema" rel="noopener noreferrer"&gt;we wrote comprehensive documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  What’s next
&lt;/h1&gt;

&lt;p&gt;FormKit is not feature-complete. Not even close. Although we've achieved feature parity with Vue Formulate (and surpassed it in several important ways), there is a lot more we are working on. Here are some of the highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input masking: FormKit was built with input masking in mind and we plan to release a full input masking plugin soon.&lt;/li&gt;
&lt;li&gt;Pro inputs: Our team is hard at work building high quality "synthetic inputs" — inputs that don’t come natively with HTML. Think: autocompletes, tag lists, dropdowns, address fields, repeaters and many more. These will be paid inputs and are how we plan to make the project financially sustainable.&lt;/li&gt;
&lt;li&gt;Theme builder: We support easy-to-use styling with our custom theme (called Genesis), your own CSS, or CSS-utility frameworks like Tailwind. But what if you could use a first-party theme builder to tailor a FormKit theme in-browser to match your project’s design? 🤔&lt;/li&gt;
&lt;li&gt;One more thing...well...many things actually. We’re hard at work on some killer features, but we've gotta keep some tricks up our sleeves 😉.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Sustainability
&lt;/h1&gt;

&lt;p&gt;You’ve heard me reference "we" and "team" a few times now. So who are these form-building heroes? There is a full team of 6 engineers behind FormKit: &lt;a href="https://twitter.com/0xBOYD" rel="noopener noreferrer"&gt;Andrew&lt;/a&gt;, &lt;a href="https://www.instagram.com/scaryonly/" rel="noopener noreferrer"&gt;Chris Adams&lt;/a&gt;, &lt;a href="https://twitter.com/devoidofgenius" rel="noopener noreferrer"&gt;Chris Ellinger&lt;/a&gt;, &lt;a href="https://twitter.com/luanguyen" rel="noopener noreferrer"&gt;Luan&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/sasha-milenkovic-b6a4b7168/" rel="noopener noreferrer"&gt;Sasha&lt;/a&gt; and myself &lt;a href="https://twitter.com/jpschroeder" rel="noopener noreferrer"&gt;Justin&lt;/a&gt;. We’re committed to open source and ensuring that FormKit is forever freely distributed. We have 2 plans to make FormKit a sustainable project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We are accepting sponsors of the FormKit open source project. If you find FormKit to be as useful as we do, &lt;a href="https://github.com/sponsors/formkit" rel="noopener noreferrer"&gt;please consider supporting us&lt;/a&gt;!&lt;/li&gt;
&lt;li&gt;We will soon be releasing a set of "pro" inputs — these will be paid first-party custom inputs. We’re targeting the complex inputs that no one enjoys creating themselves such as autocompletes, tag lists, wysiwygs, and fancy file uploaders. You can &lt;a href="https://formkit.com/pro-early-access" rel="noopener noreferrer"&gt;request early access today&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We’re inspired by the great example of folks like &lt;a href="https://twitter.com/adamwathan" rel="noopener noreferrer"&gt;Adam Wathan&lt;/a&gt; and &lt;a href="https://twitter.com/taylorotwell" rel="noopener noreferrer"&gt;Taylor Otwell&lt;/a&gt; who have built sustainable open source projects that the developer community will enjoy for years to come &lt;em&gt;because&lt;/em&gt; they've created adjacent products that support their development efforts. We aspire to do the same with FormKit ❤️.&lt;/p&gt;

&lt;h1&gt;
  
  
  So much more...
&lt;/h1&gt;

&lt;p&gt;There’s so much more that isn’t covered in this article so please — checkout the docs at &lt;a href="https://formkit.com/" rel="noopener noreferrer"&gt;formkit.com&lt;/a&gt;, join us and hundreds other on the FormKit &lt;a href="https://discord.gg/Vhu97pAC76" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;, and if you’re intrigued and would like to use FormKit in your own projects, throw us a star on &lt;a href="https://github.com/formkit/formkit" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;🙏 Thank you. We look forward to the road ahead together.&lt;/p&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Flow State: 10 Practical Tips for Getting In The Zone</title>
      <dc:creator>Justin Schroeder</dc:creator>
      <pubDate>Sun, 11 Apr 2021 03:38:35 +0000</pubDate>
      <link>https://dev.to/justinschroeder/flow-state-10-practical-tips-for-getting-in-the-zone-ha9</link>
      <guid>https://dev.to/justinschroeder/flow-state-10-practical-tips-for-getting-in-the-zone-ha9</guid>
      <description>&lt;p&gt;I have this recurring dream where I sit down at a piano and start playing like a virtuoso. The music flows out of my fingers at the same speed as my mind is composing the music. In my mind my dream feels like the purest act of creation. It is beautiful, but it's only a dream. On this side of consciousness I can't even play an instrument. Yet, that feeling of "pure creation" where my imagination smoothly becomes a reality is incredibly familiar. In fact, as engineers, on our best days, it's what we do for a living.&lt;/p&gt;

&lt;p&gt;If you've been a serious programmer for more than a few years, I imagine you've experienced this magic. There are a lot of names for it, flow state, deep work, in the zone, being under. However, I prefer “flow state” because it best describes the feeling I have when I'm there. The truth is, it can be as elusive and delicate as it is powerful, but the payoff is enormous — well worth the pursuit.&lt;/p&gt;

&lt;p&gt;When you're truly in flow state, your productivity skyrockets. Complex problems break down to simple steps, intricate logic becomes obvious, and innovative design patterns emerge. Maybe I’m being a bit too romantic. I mean we're just writing some code right? Nah, when I'm truly in flow state I'm &lt;em&gt;creating&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I've been programming for 21 years and, for the last five I've been cataloging my experiments regarding flow. The following recommendations come out of those five years, but these are not "scientific" in an academic sense. Just practical advice born of my own monastic pursuit of flow state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know the basics
&lt;/h2&gt;

&lt;p&gt;In the beginning, there are some fundamental prerequisites. These are not meant to be exclusionary, as anyone can achieve these skills and conditions. However, if you feel you fall short on any of them consider this as a friendly encouragement to keep on pushing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Know the Syntax&lt;/strong&gt; &lt;br&gt;
The kind of flow state we're after requires that concepts transfer smoothly into bytes. You cannot play the piano like a virtuoso if you're stabbing around for middle-c. I'm not saying you have the entire language perfectly memorized, but at a minimum the syntax needs to melt away.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Know your Environment&lt;/strong&gt; &lt;br&gt;
You need to have a familiar and stable development environment: operating system, editor, terminal, server stack etc. Be very cautious about messing with these precious tools. Don't install a new shell environment, use &lt;strong&gt;one&lt;/strong&gt; editor, and don’t be the first person to upgrade your OS. Keep your work space stable and familiar at all times. If you want to experiment, use a different machine or try setting up a secondary account on your computer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Know your key commands&lt;/strong&gt; &lt;br&gt;
Goes without saying right? But seriously, you should be able to go for significant stretches of time in a code editor without ever lifting your hands off the keyboard.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Know How to "long-cycle"&lt;/strong&gt; &lt;br&gt;
When writing code, in any language or platform — there is a "cycle":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You write some code.&lt;/li&gt;
&lt;li&gt;You check to see if it did what you wanted.&lt;/li&gt;
&lt;li&gt;You move on or adjust the code and repeat.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beginners typically write very small chunks of code, a couple lines for example, and then check the results. This cycling is necessary to reduce mental overhead (you can't hold all the code for an app in your mind at once), but in my experience, very small cycles also reduce the likelihood you'll reach “flow state.” It's a mental crutch that prevents your mind from learning how to balance more complex problems. Each cycle takes an amount of time to complete, even one or two seconds per cycle can really add up. This slows your progress and introduces opportunities for distraction. If you're a victim of “short cycling”, try to intentionally practice writing more code each time before you validate the results.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Know &lt;em&gt;how&lt;/em&gt; it works.&lt;/strong&gt; It's important to understand the basics of the technologies you're working with. For example, if you're a web developer, you should really understand how the request/response cycle works, what HTTP methods are, how to interact with the Document Object Model (DOM), what event bubbling is etc. Sure you may use libraries that abstract these concepts away from you, but you must have an understanding of the underlying technologies. If not, it will be difficult to create a mental sandbox. A place where your mind can quickly and efficiently conceive new solutions to challenging problems.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're struggling with any of these basics it doesn't mean you can't be productive or produce excellent code. It just means the magic is harder to grab a hold of. The good news is practice makes perfect. Keep at it!&lt;/p&gt;

&lt;h2&gt;
  
  
  10 Practical Tips
&lt;/h2&gt;

&lt;p&gt;Your brain hates getting into a flow state. What it &lt;em&gt;wants&lt;/em&gt; are quick little dopamine hits that are addictive and supremely detrimental to productivity (lookin' at you Twitter). Flow is a marathon that your brain is reluctant to run. So what follows is a list of tips, tricks, and hacks that I've found help drag my mind, kicking and screaming, to the start line. These may seem trivial...because they are. Yet these sorts of small habits and patterns are tremendously useful in combating hours of half-focused waste.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Know what you're going to work on.&lt;/strong&gt; This one is important. Before you even sit down you should already know what your first keystrokes are going to be. To help, each time you stop working, write a short todo list on a scrap of paper or a notes app (&lt;em&gt;not&lt;/em&gt; your team's Trello or Jira board) of what you need to work on next time you sit down. It's good for these to crescendo from small to large: 1. "Finish the primary navigation breakpoints" 2. "Write a click-away handler for the dropdown" 3. "make a new modal component". It can even be a good idea to even leave yourself a breadcrumb, such as an easy to solve problem that only takes a few lines of code to complete that’s been &lt;em&gt;obviously&lt;/em&gt; left incomplete for you (by you). This little trick can really jump start your day's work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Close your apps.&lt;/strong&gt; Shut down email, Slack, discord, social media and anything else that can send you a notification. Seriously. Shut 👏 Them 👏 Down 👏. Nervous someone will need you? Tell your co-workers to call you on the phone if it's really an emergency — trust me they won't call. People hate making phone calls. Those little red bubbles and pinging sounds are the death of flow state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Music.&lt;/strong&gt; This is controversial, but I've always had my best sessions &lt;em&gt;with&lt;/em&gt; music. I've found it's best to put something on that is well-worn. You already know the words and can get into the rhythm instantly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ride the wave.&lt;/strong&gt; Once you get into flow, ride the wave as far as it takes you, and then get off when it's over. Typically, I've found ~3-4 hours is as long as I can go before my mental state begins to crumble, but while I'm in flow I try hard to stay there even if it’s inconvenient. Occasionally the zen-like magic strikes me at 11:00pm and I make the conscious decision to ride it till it runs out, around 2:00- 3:00am. I'm not suggesting this is a good time to work, as you need to be well rested, but I am saying these sessions are so valuable (and frankly rare) that when they strike, it is worth the sacrifice to stay in them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instant on.&lt;/strong&gt; The moment you sit down and the screen turns on, start working. Literally do nothing else. Don't check email one last time, or order that thing off Amazon. — You can do all that later. Start writing code...now. Your brain will push back and try to distract you — you need to win this argument. Your brain will thank you later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don’t reward yourself.&lt;/strong&gt; Once in flow you'll start conquering problems left and right, but watch out! These little victories are a signal to your brain to chill out and celebrate the good times. You will suddenly find yourself randomly disengaging. — Don't fall for it. As soon as you solve one problem, instantly jump to the next, then the next. Let the progress snowball until your efficiency starts to die off.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Know how much to sleep.&lt;/strong&gt; Don't just Google "how much sleep do I need to sleep", you're a unique human being. My wife needs a solid nine hours every single night, I need about six. Everyone is different. Listen to your body, and then give it what it needs. If you're thinking about sleep while you're trying to program — you didn't get enough.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Temperature.&lt;/strong&gt; The temperature should be comfortable, but I've found that being slightly cold is better than being slightly hot.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wear comfortable clothes.&lt;/strong&gt; I remember high school teachers telling me to wear comfortable clothes when taking the SAT. They were right, although, perhaps "comfortable" isn't quite the right word. The important thing is not to be conscious of your clothes (positive or negative), you don't want to think about your clothes at all.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hydrate &amp;amp; caffeinate.&lt;/strong&gt; Drinking a glass of water ahead of time, and having coffee (if that's your thing) hot and ready is key. You don't want to have to get up for a while, so make sure your coffee is in a mug that doesn't get cold. I'm not saying that specifically coffee is needed but rather that it is important to consider and address your body's needs ahead of time so you don't get distracted. For me that “need” happens to be coffee, but for you that might be tea, fruit, or just a cup of water.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  There’s more to work than flow
&lt;/h2&gt;

&lt;p&gt;This article, rightfully, glorifies flow state. However, it's important to realize there is more to work than what occurs in flow. As developers, we're positioned in a market where our ability to rapidly produce high quality code is a great asset, but it's not our only one. Meetings, emails, code reviews, helping team members, writing blog posts and other important tasks are part of everyone's job and generally don't require the same level of hyper focus to accomplish. That's okay. When it comes to flow state, keep your powder dry, and your aim narrow. Don't get too down on yourself if you can't achieve flow state every day. While that's an admirable goal, our brains just won't always get there. Remember, there is plenty of other work that can be accomplished as a mere mortal too.&lt;/p&gt;

&lt;p&gt;I hope you find these techniques helpful. If you're interested in hearing from me when you're &lt;em&gt;not&lt;/em&gt; in flow you can &lt;a href="https://twitter.com/jpschroeder" rel="noopener noreferrer"&gt;follow me on Twitter @jpschroeder&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>webdev</category>
      <category>career</category>
      <category>programming</category>
    </item>
    <item>
      <title>Better uploads with Vue Formulate, S3, and Lambda</title>
      <dc:creator>Justin Schroeder</dc:creator>
      <pubDate>Thu, 14 Jan 2021 21:57:44 +0000</pubDate>
      <link>https://dev.to/justinschroeder/better-uploads-with-vue-formulate-s3-and-lambda-58b8</link>
      <guid>https://dev.to/justinschroeder/better-uploads-with-vue-formulate-s3-and-lambda-58b8</guid>
      <description>&lt;p&gt;Not many developers enjoy building forms — and even the oddballs who say they do don't enjoy file uploads (or they're lying 🤷‍♂️). It's a universal experience — file uploads are a pain, and worse — after all the necessary technical work the end user experience is still typically poor.&lt;/p&gt;

&lt;p&gt;Gather around friends, today I'd like to share another way to upload files that makes writing file uploads as easy as &lt;code&gt;&amp;lt;FormulateInput type="file" /&amp;gt;&lt;/code&gt;, provides a slick user experience, and requires no server-side code (well — AWS Lambdas are technically servers...ehh, you get the idea).&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F56y08o88i3uumg1glv9z.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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F56y08o88i3uumg1glv9z.gif" alt="upload" width="600" height="226"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;This is a long article, but the end results are worth it. Here's what we'll be covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The old way of doing file uploads&lt;/li&gt;
&lt;li&gt;
A better way of doing file uploads with Vue Formulate and AWS

&lt;ul&gt;
&lt;li&gt;Setting up an AWS Account&lt;/li&gt;
&lt;li&gt;Creating an S3 Storage Bucket&lt;/li&gt;
&lt;li&gt;Configuring CORS for your S3 Bucket&lt;/li&gt;
&lt;li&gt;Creating an IAM role&lt;/li&gt;
&lt;li&gt;Creating a Lambda and accompanying API&lt;/li&gt;
&lt;li&gt;Publishing Lambda function code&lt;/li&gt;
&lt;li&gt;Configuring an API Gateway&lt;/li&gt;
&lt;li&gt;Testing the upload endpoint&lt;/li&gt;
&lt;li&gt;Adding a custom uploader function to Vue Formulate&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;A working example&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;See? It's a lot, but remember the end result is &lt;code&gt;&amp;lt;FormulateInput type="file" /&amp;gt;&lt;/code&gt; resulting in direct uploads to AWS S3. Stick with me and we'll make it through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ye olde way
&lt;/h2&gt;

&lt;p&gt;In the ye olde days we uploaded files by slapping one or more &lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt; inputs in a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; that included the HTML attribute &lt;code&gt;enctype="multipart-form-data"&lt;/code&gt;. This did all the hard work of buttoning up the file for us and submitting it to our backend. Our backend code would then handle those files and usually place them somewhere on the filesystem. For example, here is a PHP script (&lt;a href="https://www.php.net/manual/en/features.file-upload.post-method.php" rel="noopener noreferrer"&gt;from the official PHP docs&lt;/a&gt;) that handles a file upload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="nv"&gt;$uploaddir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/var/www/uploads/'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$uploadfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$uploaddir&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_FILES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'userfile'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'name'&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="nb"&gt;move_uploaded_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_FILES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'userfile'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'tmp_name'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$uploadfile&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"File is valid, and was successfully uploaded.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Possible file upload attack!&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&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;Nice — so we can see PHP magically created some kind of temporary file with the contents of the uploaded file, and we move that temporary file to a permanent location on the filesystem (if we want to keep the file). This methodology still works today across various platforms, so why is it passé? Let’s highlight some of the ways this simple approach falls short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is no user feedback that the file is uploading. No progress bar, no loading animations, no disabled submit button. The user just sits there waiting for the form to submit. Have a lot of files? Your user will definitely get confused and click that submit button multiple times. Neato 👌&lt;/li&gt;
&lt;li&gt;If there's an issue with the file upload, the user won't find out till &lt;em&gt;after&lt;/em&gt; they waited for the entire upload to complete.&lt;/li&gt;
&lt;li&gt;Your backend needs to be configured to handle file uploads. For PHP this requires configuring &lt;code&gt;php.ini&lt;/code&gt; variables like &lt;code&gt;upload_max_filesize&lt;/code&gt;, &lt;code&gt;post_max_size&lt;/code&gt; and &lt;code&gt;max_input_time&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;If you're using a node server you need to be even more careful with uploads. Due to the single-threaded nature of node you can easily cause your server to run out of memory and crash. &lt;/li&gt;
&lt;li&gt;If you're using a serverless stack your backend won't even have a filesystem to store the uploads on (thats where this article comes in handy 👍).&lt;/li&gt;
&lt;li&gt;Your servers have a finite amount of disk space and it will eventually run out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of these issues can be solved by passing the file "through" your server and then on to a cloud service like S3. For example, the PHP code above could use a stream wrapper to pass the file through to an S3 bucket instead of the local filesystem. However, this is effectively double-uploading — 1) the client uploads the file to your server 2) then your server uploads the file to S3.&lt;/p&gt;

&lt;p&gt;An even better user experience is to upload files via &lt;code&gt;fetch&lt;/code&gt; or &lt;code&gt;XMLHttpRequest&lt;/code&gt; (&lt;code&gt;XMLHttpRequest&lt;/code&gt; is still preferred since &lt;code&gt;fetch&lt;/code&gt; doesn't support progress updates). However, rigging up these AJAX uploaders is a lot of work even when using pre-existing libraries and they come with their own backend shortcomings.&lt;/p&gt;

&lt;h2&gt;
  
  
  There's another way
&lt;/h2&gt;

&lt;p&gt;What if our backend servers never touched the file uploads at all? What if we could upload our files &lt;em&gt;directly&lt;/em&gt; to our cloud provider from the client's browser? What if our backend/database only stored the URL to the uploaded file? &lt;/p&gt;

&lt;p&gt;Vue Formulate allows you to turbo-charge your &lt;code&gt;file&lt;/code&gt; and &lt;code&gt;image&lt;/code&gt; inputs to do just that by implementing a custom &lt;code&gt;uploader&lt;/code&gt; function. The following describes how this can be accomplished with AWS Lambda and S3. What is Vue Formulate? Glad you asked — it's the easiest way to build forms for Vue — and I wrote an &lt;a href="https://dev.to/justinschroeder/introducing-vue-formulate-truly-delightful-form-authoring-56f5"&gt;introduction article about it&lt;/a&gt; you might be interested in.&lt;/p&gt;

&lt;p&gt;To provide the best user experience, Vue Formulate handles file uploads in an opinionated way. The library handles all of the UX like creating a dropzone, showing selected files, progress bars, file validation, displaying upload errors, and pushing completed uploads into the form's model. All you need to provide is an instance of Axios or a custom uploader function that performs your desired XHR request (don't worry, we're going to work through that together in this article).&lt;/p&gt;

&lt;p&gt;By the time a user submits the form and your &lt;code&gt;@submit&lt;/code&gt; handler is called Vue Formulate has already completed any file uploads in your form and merged the file URLs into the form data. Your backend can be sent a simple JSON payload and never needs to deal with the original files themselves. Even better, with just a little work, we can make those files upload &lt;em&gt;directly&lt;/em&gt; to S3.&lt;/p&gt;

&lt;p&gt;So how does this "direct uploading" work — and how do we do it in a secure way? S3 supports a feature that allows the creation of "signed URLs", which are generated URLs that include all the necessary credentials to perform 1 pre-approved function — such as putting an object into an S3 bucket 😉! However to create these signed URLs we need some code to be executed in a secured environment — this environment could be a standard backend server, but for our purposes we're going to use a simple Lambda function. This is a great use case for Lambda as it is a small, discrete operation that only needs to be run when a user adds files to our form (no need to have a server running 24/7 waiting to perform this operation).&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Facafipg661ei2kb1pkjq.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Facafipg661ei2kb1pkjq.png" alt="lambda-uploads" width="795" height="973"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Our custom Vue Formulate uploader function will perform a few steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Collect the files to be uploaded.&lt;/li&gt;
&lt;li&gt;Request a signed upload URL from our AWS Lambda function.&lt;/li&gt;
&lt;li&gt;Upload the file(s) to our S3 bucket using the signed upload URL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once we've added our custom uploader to our Vue Formulate instance, all of our &lt;code&gt;file&lt;/code&gt; and &lt;code&gt;image&lt;/code&gt; inputs will automatically use this mechanism. Sounds good, yeah? Ok — let's get cracking!&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Setup an AWS Account
&lt;/h3&gt;

&lt;p&gt;If you don't already have an AWS account, you'll need to set one up first. This is a standard signup process — you'll need to verify yourself and provide billing information (don't worry, &lt;a href="https://aws.amazon.com/lambda/pricing/" rel="noopener noreferrer"&gt;AWS Lambda function call pricing&lt;/a&gt; and &lt;a href="https://aws.amazon.com/s3/pricing/" rel="noopener noreferrer"&gt;AWS S3 storage pricing&lt;/a&gt; are &lt;em&gt;really&lt;/em&gt; cheap).&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Flmf425i1f18felr46pvk.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Flmf425i1f18felr46pvk.png" alt="portal.aws.amazon.com_billing_signup_nc2=h_ct&amp;amp;src=header_signup&amp;amp;redirect_url=https%3A%2F%2Faws.amazon.com%2Fregistration-confirmation" width="800" height="690"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create an S3 Storage Bucket
&lt;/h3&gt;

&lt;p&gt;Use the services dropdown to navigate to S3 so that we can create a new storage bucket. You'll need to answer a series of question when creating the bucket. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; Bucket name — I generally try to pick names that &lt;em&gt;could&lt;/em&gt; be subdomains if I decide to rig up a DNS record for them in the future. For this example, I'll use &lt;code&gt;uploads.vueformulate.com&lt;/code&gt; as my bucket name.&lt;/li&gt;
&lt;li&gt;Region name (pick the one geographically closest to you)&lt;/li&gt;
&lt;li&gt;Bucket settings for Block Public Access — uncheck all of these boxes since we're going to allow public downloads. In this example, we won't be creating private file uploads, but this same process works for that use case.&lt;/li&gt;
&lt;li&gt;Bucket versioning — you can leave this disabled, it's cheaper and we'll be using random ids to ensure we don't accidentally overwrite existing files with new uploads.&lt;/li&gt;
&lt;li&gt;Tags — These are optional and only if you want to use them. These can be helpful for tracking billing costs if you are using a lot of AWS resources.&lt;/li&gt;
&lt;li&gt;Advanced Settings - Leave "Object Lock" disabled.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Configure CORS for the bucket
&lt;/h3&gt;

&lt;p&gt;Next, we need to ensure that we configure CORS for the bucket to enable our direct uploading. In this case I'm going to apply a liberal &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; since I want my example to work from any domain. You can be more specific with your access control if you want to limit which domains are allowed to upload files to your S3 storage bucket.&lt;/p&gt;

&lt;p&gt;Click on your bucket, then select "Permissions" in the tab bar. Scroll down to "Cross-origin resource sharing", click "Edit", and enter the following JSON configuration. Finally, hit "Save Changes":&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AllowedHeaders"&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="s2"&gt;"Content-Type"&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;"AllowedMethods"&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="s2"&gt;"PUT"&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;"AllowedOrigins"&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="s2"&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;"ExposeHeaders"&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="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;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fawtjnk1wgw9bi9dt37cm.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fawtjnk1wgw9bi9dt37cm.png" alt="s3.console.aws.amazon.com_s3_home_region=us-east-2" width="800" height="690"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;h3&gt;
  
  
  4. Create an IAM role
&lt;/h3&gt;

&lt;p&gt;Next, we'll need to create an IAM role for Lambda. Use the services menu to navigate to the IAM service (Identity Access Management). Click on roles in the sidebar and choose "Create role". Select the Lambda "use case" from the services use cases and move on to the next step. &lt;/p&gt;

&lt;p&gt;This is where we attach "policies" (basically permissions). We'll add the &lt;code&gt;AWSLambdaBasicExecutionRole&lt;/code&gt; which gives our new role the ability to run Lambda functions.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fhbwomtkolqb1f2g3h96i.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fhbwomtkolqb1f2g3h96i.png" alt="console.aws.amazon.com_iam_home_region=us-east-2" width="800" height="559"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Next, add tags if you want them (not required), and finally, give your role a name and a description you'll recognize and create the role.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1nakl3cvpyekx1pdx6zh.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1nakl3cvpyekx1pdx6zh.png" alt="console.aws.amazon.com_iam_home_region=us-east-2 (1)" width="800" height="559"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Next, we need to add the ability for this role to access the S3 bucket we created. Choose the role we just created, select "Attach policies", and then click "Create Policy" button at the top. Then follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select the S3 service&lt;/li&gt;
&lt;li&gt;Select actions &lt;code&gt;PutObject&lt;/code&gt;, and &lt;code&gt;PutObjectACL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Specify the bucket ARN, and "Any" (&lt;code&gt;*&lt;/code&gt;) object in the bucket.&lt;/li&gt;
&lt;li&gt;Review and name the policy, then create it.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F27xlqu1fhhayjuov5sg1.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F27xlqu1fhhayjuov5sg1.png" alt="Creating the S3 access policy" width="800" height="581"&gt;&lt;/a&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F2a2fbki0u1x9eixcmy2o.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F2a2fbki0u1x9eixcmy2o.png" alt="Naming the S3 access policy" width="800" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, go back to the role we created, refresh the list of policies, search for our newly created policy, and add it to the role.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F86g4x9t9gjlo5rlduhpt.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F86g4x9t9gjlo5rlduhpt.png" alt="Selecting the access policy" width="800" height="581"&gt;&lt;/a&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ffcwpi3c0hnzuomf4zs6l.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ffcwpi3c0hnzuomf4zs6l.png" alt="Role policies when complete" width="800" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Create the Lambda and API
&lt;/h3&gt;

&lt;p&gt;Use the services dropdown to search for the Lambda service. Open it, and choose "Create Function", and follow the prompts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select "Author from scratch"&lt;/li&gt;
&lt;li&gt;Choose a function name, for this example I'll use "VueFormulateUploadSigner".&lt;/li&gt;
&lt;li&gt;Change the execution role and select "Use existing Role". Choose the new role that we created in the previous step.&lt;/li&gt;
&lt;li&gt;Leave the advanced settings unchanged and create the function.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fugpenww9qq1vw4pjau4i.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fugpenww9qq1vw4pjau4i.png" alt="Lambda creation screen" width="616" height="679"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Remember, this Lambda function is responsible for creating our signed upload URL, so we need an endpoint to trigger the lambda's execution. To do this, click the "+ add trigger" button, select "API Gateway", and follow the prompts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select "Create an API"&lt;/li&gt;
&lt;li&gt;For "API type" choose "HTTP API"&lt;/li&gt;
&lt;li&gt;For security, select "open" (You can always come back and add JWT later if it's needed for your specific application)&lt;/li&gt;
&lt;li&gt;Leave the additional settings blank and "Add" the gateway.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  6. Add the function code
&lt;/h3&gt;

&lt;p&gt;We need our lambda function to create a signed &lt;code&gt;putObject&lt;/code&gt; URL for us. In the Function code section double click on &lt;code&gt;index.js&lt;/code&gt;. This file is the actual code that will be executed when our Lambda is run. In this case we want to use the AWS SDK for node.js to create a signed &lt;code&gt;putObject&lt;/code&gt; URL for S3. &lt;/p&gt;

&lt;p&gt;Here's some code that does just that. You can copy and paste it directly into the code editor — although you should read through it to understand what it is doing.&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;var&lt;/span&gt; &lt;span class="nx"&gt;S3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-sdk/clients/s3&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;CORS&lt;/span&gt; &lt;span class="o"&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;Access-Control-Allow-Origin&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;*&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;Access-Control-Allow-Headers&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;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Return an error response code with a message
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;invalid&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;)&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="na"&gt;isBase64Encoded&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="nx"&gt;statusCode&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="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;message&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/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="nx"&gt;CORS&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="cm"&gt;/**
 * Generate a random slug-friendly UUID
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iterations&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;randomStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&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="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&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;iterations&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;randomStr&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;randomStr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iterations&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="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Our primary Lambda handler.
 */&lt;/span&gt;
&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&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;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="c1"&gt;// Handle CORS preflight requests&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPTIONS&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&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="nx"&gt;CORS&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Lets make sure this request has a fileName&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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// First, let's do some basic validation to ensure we recieved proper data&lt;/span&gt;
    &lt;span class="k"&gt;if &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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extension&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;invalid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Request must include "extension" and "mime" properties.&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="cm"&gt;/**
     * We generate a random filename to store this file at. This generally good
     * practice as it helps prevent unintended naming collisions, and helps
     * reduce the exposure of the files (slightly). If we want to keep the name
     * of the original file, store that server-side with a record of this new
     * name.
     */&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;uuid&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * These are the configuration options that we want to apply to the signed
     * 'putObject' URL we are going to generate. In this case, we want to add
     * a file with a public upload. The expiration here ensures this upload URL
     * is only valid for 5 minutes.
     */&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="p"&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;BUCKET_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ACL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public-read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * Now we create a new instance of the AWS SDK for S3. Notice how there are
     * no credentials here. This is because AWS will automatically use the
     * IAM role that has been assigned to this Lambda runtime.
     * 
     * The signature that gets generated uses the permissions assigned to this
     * role, so you must ensure that the Lambda role has permissions to
     * `putObject` on the bucket you specified above. If this is not true, the
     * signature will still get produced (getSignedUrl is just computational, it
     * does not actually check permissions) but when you try to PUT to the S3
     * bucket you will run into an Access Denied error.
     */&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;S3&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;signatureVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="cm"&gt;/**
         * Now we create the signed 'putObject' URL that will allow us to upload
         * files directly to our S3 bucket from the client-side.
         */&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uploadUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;putObject&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;resolve&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="c1"&gt;// Finally, we return the uploadUrl in the HTTP response&lt;/span&gt;
        &lt;span class="k"&gt;return&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="nx"&gt;CORS&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&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="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;uploadUrl&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;// If there are any errors in the signature generation process, we&lt;/span&gt;
        &lt;span class="c1"&gt;// let the end user know with a 500.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;invalid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unable to create the signed URL.&lt;/span&gt;&lt;span class="dl"&gt;'&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="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;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ff18nrhvuw6ctwohfqanz.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ff18nrhvuw6ctwohfqanz.png" alt="Adding the code to the function" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you add this code, click "Deploy". Now — the last thing we need to do in Lambda is add the &lt;code&gt;BUCKET_NAME&lt;/code&gt; environment variable. &lt;/p&gt;

&lt;p&gt;Scroll down from the code editor and choose "Edit" under environment variables. Enter a new key &lt;code&gt;BUCKET_NAME&lt;/code&gt; and set the value to our S3 bucket name (I chose &lt;code&gt;uploads.vueformulate.com&lt;/code&gt; as my name). Hit save, and your Lambda is ready to go!&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fya3x901dazhdlugfpxfi.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fya3x901dazhdlugfpxfi.png" alt="The BUCKET_NAME environment variable" width="757" height="187"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Configure the API Gateway
&lt;/h3&gt;

&lt;p&gt;We're getting close! Before we can start sending HTTP traffic to our Lambda we need to configure the API Gateway we created. &lt;/p&gt;

&lt;p&gt;Navigate to the API gateway service and you should see a service with the same name as our Lambda with an &lt;code&gt;-API&lt;/code&gt; suffix — let's click into that. The API Gateway service is a powerful utility that makes it easy to configure which Lambdas respond to which API requests. If you chose "Develop &amp;gt; Routes" you'll see that our Lambda has already attached itself to the &lt;code&gt;/{lambdaName}&lt;/code&gt; route.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fu6gr02t0mg5jdt8tcyko.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fu6gr02t0mg5jdt8tcyko.png" alt="API Gateway Routes" width="800" height="487"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Personally, I prefer this route to be something more like &lt;code&gt;/signature&lt;/code&gt;. We can easily change it, and while we're at it, let's restrict this endpoint to only respond to POST requests.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fia7puhmzls354dukhwi2.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fia7puhmzls354dukhwi2.png" alt="Editing a route" width="423" height="247"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's a problem though. Since we've restricted the endpoint to &lt;code&gt;POST&lt;/code&gt; only, the browser's CORS &lt;code&gt;OPTIONS&lt;/code&gt; preflight requests will fail. &lt;/p&gt;

&lt;p&gt;Let's add another route for the same &lt;code&gt;/signature&lt;/code&gt; path that also points to our Lambda (our code there will handle the CORS request). Create the route, and then click "Create and attach an integration" on the for the &lt;code&gt;OPTIONS&lt;/code&gt; route and follow the prompts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select "Lambda function" for the integration type.&lt;/li&gt;
&lt;li&gt;Select the region and function of our Lambda.&lt;/li&gt;
&lt;li&gt;Create the integration.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fowsrohfwxfe5fmp9ahjb.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fowsrohfwxfe5fmp9ahjb.png" alt="Creating the OPTIONS route" width="751" height="975"&gt;&lt;/a&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Foik5w278sb1qj9mqfx2f.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Foik5w278sb1qj9mqfx2f.png" alt="Routes after configuration" width="755" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When making changes to this default API, the changes are auto-deployed on the default "stage". You can think of stages like environments. Adding multiple stages here is beyond the scope of what we're doing here. For such a simple function using the default stage is perfectly fine.&lt;/p&gt;

&lt;p&gt;If you navigate back to the main page for this API, you'll see we have an "invoke URL" for &lt;code&gt;$default&lt;/code&gt; — this is your new APIs URL!&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F4kegxqrz5bo8c88hxi78.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F4kegxqrz5bo8c88hxi78.png" alt="The API Endpoint" width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(You can change this to a custom domain if you wish, but this guide doesn't focus on that)&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Test your endpoint!
&lt;/h3&gt;

&lt;p&gt;Phew — that took some doing, but we should be up and running at this point. To test, copy the "invoke URL" and append &lt;code&gt;/signature&lt;/code&gt; to the end of it. Let's try to ping our endpoint with a cURL request. Be sure to replace the values with your own endpoint values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"extension": "pdf", "mime": "application/json"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-X&lt;/span&gt; POST https://cq2cm6d0h6.execute-api.us-east-1.amazonaws.com/signature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should get back a JSON response with a signed URL:&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="nl"&gt;"uploadUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"https://s3.amazonaws.com/uploads.vueformulate.com/hf8wj10h5svg3irf42gf.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Credential=ASIA2EL2NL4LVYXJTOK2%2F20210105%2Fus-east-1%2Fs3%2Faws4_request&amp;amp;X-Amz-Date=20210105T165545Z&amp;amp;X-Amz-Expires=300&amp;amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEFEaCXVzLWVhc3QtMiJHMEUCICwx61VITKOKB77AbzOBYFQ54hPigpS8YjXBn3opFCBHAiEAw4bNSBBAnrugIVs0kxFgFU%2Bxich0WrN%2BS96WJBqLt%2BYq1wEIGhAAGgw2OTY1NzgzMDE3MTkiDJLL6F8ULm9ygw6pIyq0Ac1moVu2PgGMBz4th89uCWW6XUsUAD%2FNcY5JUf06%2Btl2LU7j9DjxLtm5fKt0Bkc6Z96U03HmP4job3vYTKXR2wQPaW381fd5UKQLgiqz3o4ENwg8E92unTtZZ8DrP4yjntkkqUrw8Ybavyrik2eAPnp2ME%2FQe2kzl85rBWFgQdHj8bXBYPxgV1dIGyAi%2BQtp0XMMcJyJNR5Lgdh05py3WEpf0mCVS3vBe1MJP3m6kph7OMZLWDCnsNL%2FBTrgAQplCeuZMLkutxSWG8KHYUUGB5fLkJQJtK4xJab4zmxBndNXRT4tPLDPpiyyX%2B25DQbAxD48azztgdEOOim8%2BnY6rZTsY7KTd1%2FuQwryAr%2Bt9rzvL0ubkCo3PWK1UD0TBhx%2BjpE1KPyYjA4df0xlQyx0D1ee0uVRthn9FY9bDkuN8EWs2KNVxbt%2BbWYxAUJ5mqOtq1zWWa%2BXTWR20BlzWGG8NZTy0krkp9mBLM1mPIHdVNpgbgdMsnW3L0UtZXpCYT8n1QpVsMnIDuYcAK3ogOYLcIq0KOK8PWOk6whbz39W&amp;amp;X-Amz-Signature=362c8bc5cb11d6b5a14c52f82b58c25eae56b70bfaf22e01b25ac4ba4436b71e&amp;amp;X-Amz-SignedHeaders=host%3Bx-amz-acl&amp;amp;x-amz-acl=public-read"&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;Success! Our Lambda code creates upload URLs that expire after 5 minutes — this isn't a problem since Vue Formulate will use the signed url immediately, but if you're playing around with the URL by hand it's worth keeping the expiration limit in mind.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: The above CURL request is an actual live lambda I manage, feel free test with it, be be aware that all files are automatically deleted after 24 hours 👍&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  9. The uploader function
&lt;/h3&gt;

&lt;p&gt;The last step in our process is writing a custom uploader for Vue Formulate. Remember, when Vue Formulate receives a file from the end user it passes that file off to an uploader function (or axios). We want to use a custom implementation of the uploader function to fetch a signed URL and then perform an &lt;code&gt;XMLHttpRequest&lt;/code&gt; (xhr) to that URL with our file data. The implementation details of this will vary ever so slightly depending on the specifics of your project but here's how this can be done globally via a Vue Formulate plugin:&lt;/p&gt;

&lt;h4&gt;
  
  
  s3-uploader-plugin.js
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;uploadToS3&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;progress&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;options&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;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&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="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.([&lt;/span&gt;&lt;span class="sr"&gt;a-zA-Z0-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&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;extension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;matches&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="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;txt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nf"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploadUrl&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="s1"&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="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="na"&gt;body&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;extension&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/octet-stream&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;if &lt;/span&gt;&lt;span class="p"&gt;(&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;ok&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;uploadUrl&lt;/span&gt; &lt;span class="p"&gt;}&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;response&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="nf"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&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;xhr&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;XMLHttpRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;uploadUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&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="nx"&gt;loaded&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;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHeader&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/octet-stream&lt;/span&gt;&lt;span class="dl"&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;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&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;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;reject&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;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;reject&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;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nx"&gt;xhr&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="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="nf"&gt;progress&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uploadUrl&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="na"&gt;url&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&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;pathname&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&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="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="c1"&gt;// we'll suppress this since we have a catch all 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;// Catch all error&lt;/span&gt;
  &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;There was an error uploading your file.&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="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;uploader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uploadToS3&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;h4&gt;
  
  
  main.js
&lt;/h4&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="nx"&gt;Vue&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;VueFormulate&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;@braid/vue-formulate&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;S3UploaderPlugin&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;./s3-uploader-plugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Your main.js file or wherever you initialize Vue Formulate.&lt;/span&gt;

&lt;span class="nx"&gt;Vue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;VueFormulate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Use API Gateway URL + route path 😉&lt;/span&gt;
    &lt;span class="na"&gt;uploadUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://6etx7kng79.execute-api.us-east-2.amazonaws.com/signature&lt;/span&gt;&lt;span class="dl"&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;S3UploaderPlugin&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;h2&gt;
  
  
  A working example
&lt;/h2&gt;

&lt;p&gt;You're done! With those changes in place, all &lt;code&gt;file&lt;/code&gt; and &lt;code&gt;image&lt;/code&gt; inputs in your Vue Formulate instance will automatically upload their contents directly to S3 from the&lt;br&gt;
client's browser. &lt;/p&gt;

&lt;p&gt;You can use as many file uploads as you'd like on any and all forms in your project with no additional configuration. &lt;/p&gt;

&lt;p&gt;Here's an example in action:&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://codesandbox.io/embed/vue-formulate-s3-lambda-upload-example-qfynq"&gt;
&lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;If you’re intrigued, checkout &lt;a href="https://vueformulate.com/" rel="noopener noreferrer"&gt;vueformulate.com&lt;/a&gt;. You can follow me, &lt;a href="https://twitter.com/jpschroeder" rel="noopener noreferrer"&gt;Justin Schroeder&lt;/a&gt;, on twitter — as well as my co-maintainer &lt;a href="https://twitter.com/BoydDotDev" rel="noopener noreferrer"&gt;Andrew Boyd&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>aws</category>
    </item>
    <item>
      <title>Tailwind + Vue Formulate = ❤️</title>
      <dc:creator>Justin Schroeder</dc:creator>
      <pubDate>Thu, 18 Jun 2020 18:33:47 +0000</pubDate>
      <link>https://dev.to/justinschroeder/tailwind-vue-formulate-24k1</link>
      <guid>https://dev.to/justinschroeder/tailwind-vue-formulate-24k1</guid>
      <description>&lt;h1&gt;
  
  
  Using Tailwind with Vue Formulate
&lt;/h1&gt;

&lt;p&gt;Watching &lt;a href="https://www.vueformulate.com" rel="noopener noreferrer"&gt;Vue Formulate&lt;/a&gt; begin to gain traction in the Vue ecosystem in the last few months has been a real thrill. Sadly, we've also watched citizens of the Tailwind world struggle to &lt;code&gt;@apply&lt;/code&gt; their beloved styles to Vue Formulate’s internal elements. I'm happy to announce that with the release of &lt;code&gt;2.4&lt;/code&gt;, that just changed for Tailwind (and any other class-based CSS framework).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Mobile users: The demos in this article are on codesandbox which &lt;a href="https://github.com/codesandbox/codesandbox-client/issues/3646" rel="noopener noreferrer"&gt;breaks on mobile&lt;/a&gt;. If you’re on mobile, you might want to revisit on desktop.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Missions aligned
&lt;/h2&gt;

&lt;p&gt;Tailwind’s core concept of writing “HTML instead of CSS” is aimed at improving the developer experience, increasing maintainability, and making developers more efficient. Tailwind achieves this by reducing the decision making process around class names, tightly coupling styles with their usage, and abstracting away the complexity of the underlying framework.&lt;/p&gt;

&lt;p&gt;These goals are nearly identical to how Vue Formulate approaches another one of web development’s least favorite necessities: forms. Vue Formulate’s objective is to provide the best possible developer experience for creating forms by minimizing time consuming features like accessibility, validation, and error handling.&lt;/p&gt;

&lt;p&gt;In “&lt;a href="https://dev.to/justinschroeder/introducing-vue-formulate-truly-delightful-form-authoring-56f5"&gt;Introducing Vue Formulate&lt;/a&gt;,”  I described how there are several good pre-existing tools in the Vue ecosystem that handle various aspects of forms. Some of these handle validation, some handle form generation, some form bindings — Vue Formulate aims to handle all of these concerns. I believe they’re tightly coupled issues and call for a tightly coupled solution, not unlike Tailwind’s approach to styling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defaults matter
&lt;/h2&gt;

&lt;p&gt;This coupling means form inputs come with markup out of the box. The out-of-the-box DOM structure is well suited for the vast majority of forms, and for those that fall outside the bell curve, Vue Formulate supports extensive scoped slots and (“&lt;a href="https://vueformulate.com/guide/inputs/slots/#slot-components" rel="noopener noreferrer"&gt;slot components&lt;/a&gt;”). Still — defaults matter. In my own development career I've learned that, as frequently as possible, it’s wise to “prefer defaults”, only deviating when necessary (I can’t tell you how many times I’ve debugged someone's &lt;code&gt;fish&lt;/code&gt; shell because they saw a nifty article about it).&lt;/p&gt;

&lt;p&gt;Vue Formulate’s defaults are there for good reason too. Actually, lots of good reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Value added features&lt;/strong&gt;: Labels, help text, progress bars, and error messages require markup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility&lt;/strong&gt;: How often do developers remember to wire up &lt;code&gt;aria-describedby&lt;/code&gt; for their help text?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling&lt;/strong&gt;: Some elements just can’t be styled well natively and require wrappers or decorators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt;: How often do developers write tests for their project’s forms? The default markup and functionality of Vue Formulate is &lt;em&gt;heavily&lt;/em&gt; tested out of the box.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Personally, my favorite feature of Vue Formulate is that once you’ve setup your styles and customizations, the API for composing those forms is &lt;em&gt;always&lt;/em&gt; consistent.   No wrapper components, no digging through classes to apply (hm... was it &lt;code&gt;.form-control&lt;/code&gt;, &lt;code&gt;.input&lt;/code&gt;, or &lt;code&gt;.input-element&lt;/code&gt; 🤪), and no need to define scoped slots every time.&lt;/p&gt;

&lt;p&gt;So what’s the downside? Well, until now, it's been a bit tedious to add styles to the internal markup — especially if you were using a utility framework like Tailwind. Let’s take a look at how the updates in &lt;code&gt;2.4&lt;/code&gt; make styling easier than ever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining your classes (props!)
&lt;/h2&gt;

&lt;p&gt;Every DOM element in Vue Formulate’s internal markup is named. We call these names &lt;code&gt;element class keys&lt;/code&gt; — and they’re useful for targeting the exact element you want to manipulate with custom classes. Let’s start with the basics — a text input. Out of the box this input will have no styling at all (unless you install the default theme).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;FormulateInput&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;In this case, we want to spice that element up by adding some Tailwind mojo to the &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; element itself. The class key for the &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; is &lt;code&gt;input&lt;/code&gt; 🙀. Sensible defaults — what! Let’s slap some Tailwind classes on the input element by defining the new &lt;code&gt;input-class&lt;/code&gt; prop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;FormulateInput&lt;/span&gt;
  &lt;span class="na"&gt;input-class=&lt;/span&gt;&lt;span class="s"&gt;"w-full px-3 py-2 border border-gray-400 border-box rounded leading-none focus:border-green-500 outline-none"&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;&lt;iframe src="https://codesandbox.io/embed/tailwind-vue-formulate-70bhb?module=/src/components/Demo-01.vue&amp;amp;runonclick=1"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Ok! That’s a start, but Vue Formulate wouldn’t be very useful if that’s all it was. Time to flex. Let’s make a password reset form with a dash of validation logic, and for styling we’ll use the &lt;code&gt;input-class&lt;/code&gt; prop we defined above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;FormulateForm&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"values"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;submit=&lt;/span&gt;&lt;span class="s"&gt;"submitted"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl mb-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Password reset&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;FormulateInput&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
    &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
    &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"New password"&lt;/span&gt;
    &lt;span class="na"&gt;help=&lt;/span&gt;&lt;span class="s"&gt;"Pick a new password, must have at least 1 number."&lt;/span&gt;
    &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"^required|min:5,length|matches:/[0-9]/"&lt;/span&gt;
    &lt;span class="na"&gt;:validation-messages=&lt;/span&gt;&lt;span class="s"&gt;"{
      matches: 'Password must contain at least 1 number.'
    }"&lt;/span&gt;
    &lt;span class="na"&gt;input-class=&lt;/span&gt;&lt;span class="s"&gt;"border border-gray-400 rounded px-3 py-2 leading-none focus:border-green-500 outline-none border-box w-full"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;FormulateInput&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
    &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password_confirm"&lt;/span&gt;
    &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Confirm password"&lt;/span&gt;
    &lt;span class="na"&gt;help=&lt;/span&gt;&lt;span class="s"&gt;"Just re-type what you entered above"&lt;/span&gt;
    &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"^required|confirm"&lt;/span&gt;
    &lt;span class="na"&gt;validation-name=&lt;/span&gt;&lt;span class="s"&gt;"Password confirmation"&lt;/span&gt;
    &lt;span class="na"&gt;input-class=&lt;/span&gt;&lt;span class="s"&gt;"border border-gray-400 rounded px-3 py-2 leading-none focus:border-green-500 outline-none border-box w-full"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;FormulateInput&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;
&lt;span class="nt"&gt;&amp;lt;/FormulateForm&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;iframe src="https://codesandbox.io/embed/tailwind-vue-formulate-2-password-reset-unstyled-qe3cq?module=/src/components/Demo-02.vue&amp;amp;runonclick=1"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Ok, clearly it needs a little more styling. We’re dealing with a lot more DOM elements than just the text input now. Fortunately, &lt;a href="https://vueformulate.com/guide/theming/customizing-classes/#class-keys" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt; for our element keys makes these easily identifiable.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fey3udgg3dt18pjq4z50j.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fey3udgg3dt18pjq4z50j.png" alt="Anatomy of a FormulateInput component" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So it seems we need to define styles for the &lt;code&gt;outer&lt;/code&gt;, &lt;code&gt;label&lt;/code&gt;, &lt;code&gt;help&lt;/code&gt;, and &lt;code&gt;error&lt;/code&gt; keys too. Let’s try this again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;FormulateInput&lt;/span&gt;
  &lt;span class="err"&gt;...&lt;/span&gt;
  &lt;span class="na"&gt;outer-class=&lt;/span&gt;&lt;span class="s"&gt;"mb-4"&lt;/span&gt;
  &lt;span class="na"&gt;input-class=&lt;/span&gt;&lt;span class="s"&gt;"border border-gray-400 rounded px-3 py-2 leading-none focus:border-green-500 outline-none border-box w-full mb-1"&lt;/span&gt;
  &lt;span class="na"&gt;label-class=&lt;/span&gt;&lt;span class="s"&gt;"font-medium text-sm"&lt;/span&gt;
  &lt;span class="na"&gt;help-class=&lt;/span&gt;&lt;span class="s"&gt;"text-xs mb-1 text-gray-600"&lt;/span&gt;
  &lt;span class="na"&gt;error-class=&lt;/span&gt;&lt;span class="s"&gt;"text-red-700 text-xs mb-1"&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;&lt;iframe src="https://codesandbox.io/embed/tailwind-vue-formulate-2-password-reset-styled-t1k0h?module=/src/components/Demo-02.vue&amp;amp;runonclick=1"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Ok, that’s looking much better. But while it’s a relief for our eyes, the beauty is only skin deep. Those were some gnarly class props and we had to copy and paste them for both our inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining your classes (base classes!)
&lt;/h2&gt;

&lt;p&gt;So what’s a Tailwinder to do? Wrap these components in a higher order component, right!? Heck no. Please, &lt;em&gt;please&lt;/em&gt; don’t do that. While wrapping is sometimes the right choice, Vue Formulate is clear that it’s an anti-pattern for your &lt;code&gt;FormulateInput&lt;/code&gt; components. Why? Well lots of reasons, but just to name a few:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It makes props unpredictable. Did you remember to pass them all through? Will you update all your HOCs to support newly released features?&lt;/li&gt;
&lt;li&gt;Form composition no longer has a unified API. Now you need to start naming, remembering, and implementing custom components.&lt;/li&gt;
&lt;li&gt;You can no longer use schema defaults when &lt;a href="https://vueformulate.com/guide/forms/generating-forms/#schemas" rel="noopener noreferrer"&gt;generating forms&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So let’s avoid this Instant Technical Debt™ and instead use Vue Formulate’s global configuration system. We can define all of the above Tailwind classes when we first register Vue Formulate with Vue.&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="nx"&gt;Vue&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;VueFormulate&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-formulate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;Vue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;VueFormulate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mb-4&lt;/span&gt;&lt;span class="dl"&gt;'&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;border border-gray-400 rounded px-3 py-2 leading-none focus:border-green-500 outline-none border-box w-full mb-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;font-medium text-sm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;help&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-xs mb-1 text-gray-600&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="s1"&gt;text-red-700 text-xs mb-1&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;That really cleans up our inputs!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;FormulateInput&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
  &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
  &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"New password"&lt;/span&gt;
  &lt;span class="na"&gt;help=&lt;/span&gt;&lt;span class="s"&gt;"Pick a new password, must have at least 1 number."&lt;/span&gt;
  &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"^required|min:5,length|matches:/[0-9]/"&lt;/span&gt;
  &lt;span class="na"&gt;:validation-messages=&lt;/span&gt;&lt;span class="s"&gt;"{
    matches: 'Password must contain at least 1 number.'
  }"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;FormulateInput&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
  &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password_confirm"&lt;/span&gt;
  &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Confirm password"&lt;/span&gt;
  &lt;span class="na"&gt;help=&lt;/span&gt;&lt;span class="s"&gt;"Just re-type what you entered above"&lt;/span&gt;
  &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"^required|confirm"&lt;/span&gt;
  &lt;span class="na"&gt;validation-name=&lt;/span&gt;&lt;span class="s"&gt;"Password confirmation"&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;&lt;iframe src="https://codesandbox.io/embed/tailwind-vue-formulate-2-password-reset-global-styled-ktqj1?module=/src/main.js&amp;amp;runonclick=1"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;If you viewed the working code in CodeSandbox, you might have noticed we’re still using the &lt;code&gt;input-class&lt;/code&gt; prop on the submit button — and to be crystal clear — setting classes with props is not discouraged at all. Generally you’ll want to pre-configure default Tailwind classes for all of your inputs first and then use class props for selective overrides. &lt;/p&gt;

&lt;p&gt;In this case, however, the desired styles for our password input is nothing like our submit button. To account for this, we can change our &lt;code&gt;classes.input&lt;/code&gt; option to be a function instead of a string allowing us to dynamically apply classes based on contextual information.&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="nx"&gt;Vue&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;VueFormulate&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-formulate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;Vue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;VueFormulate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mb-4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;input &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="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;switch &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;classification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px-4 py-2 rounded bg-green-500 text-white hover:bg-green-600&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;border border-gray-400 rounded px-3 py-2 leading-none focus:border-green-500 outline-none border-box w-full mb-1&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;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;font-medium text-sm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;help&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-xs mb-1 text-gray-600&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="s1"&gt;text-red-700 text-xs mb-1&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;We can use Vue Formulate’s “classifications” from the provided &lt;code&gt;context&lt;/code&gt; object to change which classes are returned. These class functions give efficient, precise, reactive control over the classes you want to generate for any input (in any state). For more details on how to leverage them, checkout &lt;a href="https://vueformulate.com/guide/theming/customizing-classes/" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our example form is now fully styled, and our inputs contain no inline classes or class prop declarations at all. Any additional &lt;code&gt;FormulateInput&lt;/code&gt; will now also have base styles. Great success!&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://codesandbox.io/embed/tailwind-vue-formulate-2-password-reset-all-global-kydyp?module=/src/main.js&amp;amp;runonclick=1"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Oh, the places you’ll go
&lt;/h2&gt;

&lt;p&gt;There’s a lot more to love about the new class system in Vue Formulate that is covered in &lt;a href="https://vueformulate.com/guide/theming/customizing-classes/" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt;. You can easily reset, replace, extend, and manipulate classes on any of your form inputs. You can apply classes based on the type of input, the validation state of an input, or whenever or not a value equals “Adam Wathan”. To top it off, once you’ve landed on a set of utility classes for your project, you can package them up into your own plugin for reuse on other projects or to share with the world.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dropping the mic
&lt;/h2&gt;

&lt;p&gt;One last demo for the road? Great! Let’s combine Tailwind with another Vue Formulate fan favorite: form generation. With this feature, you can store your forms in a database or CMS and generate them on the fly with a simple schema and 1 line of code. First our schema, which is just a JavaScript 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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&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="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;h3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-2xl mb-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Order pizza&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;select&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pizza size&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="s2"&gt;size&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Select a size&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;small&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Small&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;large&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Large&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;extra_large&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Extra Large&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;required&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;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;children&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cheese&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cheese options&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;min:1,length&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;mozzarella&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mozzarella&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;feta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Feta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;parmesan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Parmesan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Extra cheese&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;outer-class&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;w-1/2&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;toppings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Toppings&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;min:2,length&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;salami&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Salami&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;prosciutto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Prosciutto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;avocado&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Avocado&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;onion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Onion&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;outer-class&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;w-1/2&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="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;children&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;select&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="s2"&gt;country_code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Code&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&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;outer-class&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;w-1/4 mr-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;options&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;1&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;+1&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;49&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;+49&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;55&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;+55&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Phone number&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="s2"&gt;phone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;inputmode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;numeric&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[0-9]*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;matches:/^[0-9-]+$/&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;outer-class&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex-grow&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;validation-messages&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;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Phone number should only include numbers and dashes.&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="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="s2"&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;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Order pizza&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 our single line of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;FormulateForm&lt;/span&gt; &lt;span class="na"&gt;:schema=&lt;/span&gt;&lt;span class="s"&gt;"schema"&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;Presto! Your form is ready.&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://codesandbox.io/embed/tailwind-vue-formulate-3-form-generation-egn58?module=/src/components/Demo-03.vue&amp;amp;runonclick=1"&gt;
&lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;If you’re intrigued, checkout &lt;a href="https://vueformulate.com/" rel="noopener noreferrer"&gt;vueformulate.com&lt;/a&gt;. You can follow me, &lt;a href="https://twitter.com/jpschroeder" rel="noopener noreferrer"&gt;Justin Schroeder&lt;/a&gt;, on twitter — as well as my co-maintainer &lt;a href="https://twitter.com/BoydDotDev" rel="noopener noreferrer"&gt;Andrew Boyd&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>vue</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>Introducing Vue Formulate — truly delightful form authoring.</title>
      <dc:creator>Justin Schroeder</dc:creator>
      <pubDate>Tue, 19 May 2020 19:38:53 +0000</pubDate>
      <link>https://dev.to/justinschroeder/introducing-vue-formulate-truly-delightful-form-authoring-56f5</link>
      <guid>https://dev.to/justinschroeder/introducing-vue-formulate-truly-delightful-form-authoring-56f5</guid>
      <description>&lt;p&gt;&lt;a href="https://vueformulate.com/" rel="noopener noreferrer"&gt;Vue Formulate&lt;/a&gt; has been in the wild for 2 months now, and with the latest release (v2.3) the project has enough momentum to warrant a post from its creator (me, &lt;a href="https://twitter.com/jpschroeder" rel="noopener noreferrer"&gt;Justin Schroeder&lt;/a&gt;) on why it exists, what it does, and where it is going.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1mly1rubhdd84nh3gyk.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1mly1rubhdd84nh3gyk.gif" alt="Quick example of Vue Formulate" width="600" height="176"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem with forms
&lt;/h3&gt;

&lt;p&gt;When you’re learning to program, one of the most exciting early progressions is when you make your "Hello World" app &lt;em&gt;interactive&lt;/em&gt; by prompting a user for their name. Take those mad I.O. skills to the web and it gets even easier! Just plop an &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; tag into your markup and you’re off the races right? Well...not so fast. &lt;/p&gt;

&lt;p&gt;Over the past two months, I've gotten a lot of questions about Vue Formulate. Unsurprisingly one of the most frequent ones is, "What’s wrong with HTML?".&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1236275673108557824-882" src="https://platform.twitter.com/embed/Tweet.html?id=1236275673108557824"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1236275673108557824-882');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1236275673108557824&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;There’s nothing &lt;em&gt;wrong&lt;/em&gt; with HTML, of course, just like there was nothing wrong with JavaScript before Vue and React (I know, I know, Vanilla purists’ blood is boiling out there). HTML, React, Vue... it doesn’t matter — the reality is: creating high-quality forms requires a lot of consideration. Labels, help text, validation, inline file uploads, and accessibility are just a few of the items a developer will need to address. This almost inevitably amounts to gobs of copy/paste and boilerplate markup littered throughout your codebase.&lt;/p&gt;

&lt;p&gt;There are other issues too. HTML validation, for example, is pretty limited. What if you want to asynchronously check if a username is already taken? What if you want to have well-styled validation errors? What if you want to offer the ability for someone to add more attendees on their ticket purchase? None of these are available to native HTML/React/Vue without considerable effort. Furthermore, maintaining a high level of quality while working on such disparate features becomes secondary to just making the form &lt;em&gt;work&lt;/em&gt;. This is fertile ground for a library to help increase developer happiness while pushing quality and accessibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is Vue Formulate different?
&lt;/h3&gt;

&lt;p&gt;Vue Formulate is far from the first library to address these concerns. Our long-time friends in the community have been fighting these battles for ages: vue-forms, VeeValidate, Vuelidate, and even some UI frameworks like Vuetify aim to help developers author better forms. These are great packages and I wouldn’t discourage you from using them if they’re appropriate for your project. However, Vue Formulate approaches the same problems with two specific objectives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Improve the developer experience of form authoring.&lt;/li&gt;
&lt;li&gt;Increase the quality of forms for end-users.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In order to provide a great developer experience, Vue Formulate needs to focus on being a &lt;em&gt;comprehensive form authoring&lt;/em&gt; solution. It cannot just be a validator and doesn’t aspire to become a full UI library. Instead, these guiding principles have resulted in a highly consistent component-first API focused solely on first-class form authoring. To that end, every single input in Vue Formulate is authored with the same component &lt;code&gt;&amp;lt;FormulateInput&amp;gt;&lt;/code&gt;, smoothing out the inconsistencies in HTML’s default elements such as &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; and others. In Vue Formulate you simply tell the &lt;code&gt;&amp;lt;FormulateInput&amp;gt;&lt;/code&gt; what type of input it should be — a text input (&lt;code&gt;&amp;lt;FormulateInput type="text"&amp;gt;&lt;/code&gt;) and a select input (&lt;code&gt;&amp;lt;FormulateInput type="select"&amp;gt;&lt;/code&gt;) can even be dynamically exchanged by changing the &lt;code&gt;type&lt;/code&gt; prop on the fly.&lt;/p&gt;

&lt;p&gt;Why is this better you ask? It’s better because it’s easy to remember, fast to compose, and reduces mistakes. We absolutely shouldn’t discount those very real quality of life improvements... but of course that’s not all.&lt;/p&gt;

&lt;p&gt;By ensuring all inputs conform to a single component interface we allow for more powerful enhancements like automatic labels, declarative validation, form generation, automatic accessibility attributes, and support for complex custom inputs. This allows a &lt;code&gt;FormulateInput&lt;/code&gt; component to maintain an easy-to-use API while being endowed with super powers. Consider how similarly these two inputs are authored using Vue Formulate and yet how different their actual HTML implementation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;FormulateInput&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;label=&lt;/span&gt;&lt;span class="s"&gt;"Enter your email address"&lt;/span&gt;
  &lt;span class="na"&gt;help=&lt;/span&gt;&lt;span class="s"&gt;"We’ll send you an email when your ice cream is ready"&lt;/span&gt;
  &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"required|email"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;FormulateInput&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt;
  &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"flavor"&lt;/span&gt;
  &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Pick your 2 favorite flavors"&lt;/span&gt;
  &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"min:2,length"&lt;/span&gt;
  &lt;span class="na"&gt;:options=&lt;/span&gt;&lt;span class="s"&gt;"{
   vanilla: 'Vanilla',
   chocolate: 'Chocolate',
   strawberry: ’Strawberry',
   apple: 'Apple'
  }"&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;&lt;iframe height="600" src="https://codepen.io/justin-schroeder/embed/dyYQZgr?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Now, notice some of the things we &lt;em&gt;didn’t&lt;/em&gt; have to deal with in that example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; elements inputs were automatically generated and linked to the &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; element via auto-generated ids (specify if you want).&lt;/li&gt;
&lt;li&gt;Help text was generated in the proper location and the input was linked to it with &lt;code&gt;aria-describedby&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We added real time input validation without having to explicitly output errors.&lt;/li&gt;
&lt;li&gt;Multiple checkboxes were rendered with their values linked together.&lt;/li&gt;
&lt;li&gt;The labels for the checkboxes automatically adjusted their position.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By consolidating inputs into a single &lt;code&gt;FormulateInput&lt;/code&gt; component, we drastically improve the quality of life for developers, and simultaneously create a powerful hook for adding new features and functionality to those inputs. As a bonus, when it comes time to upgrade to Vue 3’s Composition API, Vue Formulate’s component-first API means developers won’t need to refactor anything in their template code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Neato, but where’s my form?
&lt;/h3&gt;

&lt;p&gt;I’ve explained Vue Formulate’s purpose and its unique approach to inputs, but how about the form itself? Let’s consider the purpose of the native &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element: to transmit input from a user to a server by aggregating the values of its input elements. What does that look like in Vue Formulate? Pretty much exactly what you would expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;FormulateForm&lt;/span&gt;
    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;submit=&lt;/span&gt;&lt;span class="s"&gt;"login"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FormulateInput&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;label=&lt;/span&gt;&lt;span class="s"&gt;"Email address"&lt;/span&gt;
      &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"required|email"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FormulateInput&lt;/span&gt;
      &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
      &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
      &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Password"&lt;/span&gt;
      &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"required"&lt;/span&gt; 
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FormulateInput&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Login"&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;
  &lt;span class="nt"&gt;&amp;lt;/FormulateForm&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;login &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="p"&gt;{&lt;/span&gt;
      &lt;span class="cm"&gt;/* do something with data when it passes validation:
       * { email: 'zzz@zzz.com', password: 'xyz' }
       */&lt;/span&gt;
      &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Logged in&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="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&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;Great, so data aggregation works just like a normal form, but there’s not anything “reactive” here yet. Ahh, let’s slap a &lt;code&gt;v-model&lt;/code&gt; onto that form — and — presto! We have a fully reactive object with all the data in our form.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;FormulateForm&lt;/span&gt;
    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;submit=&lt;/span&gt;&lt;span class="s"&gt;"login"&lt;/span&gt;
    &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"values"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FormulateInput&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;label=&lt;/span&gt;&lt;span class="s"&gt;"Email address"&lt;/span&gt;
      &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"required|email"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FormulateInput&lt;/span&gt;
      &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
      &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
      &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Password"&lt;/span&gt;
      &lt;span class="na"&gt;validation=&lt;/span&gt;&lt;span class="s"&gt;"required"&lt;/span&gt; 
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FormulateInput&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Login"&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;
    &lt;span class="nt"&gt;&amp;lt;pre&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/FormulateForm&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;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="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;data &lt;/span&gt;&lt;span class="p"&gt;()&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="na"&gt;values&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;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;login &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="p"&gt;{&lt;/span&gt;
      &lt;span class="cm"&gt;/* do something with data:
       * { email: 'zzz@zzz.com', password: 'xyz' }
       */&lt;/span&gt;
      &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Logged in&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="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&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;&lt;iframe height="600" src="https://codepen.io/justin-schroeder/embed/VwvVyZo?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;And yes, &lt;code&gt;v-model&lt;/code&gt; means its &lt;em&gt;two-way&lt;/em&gt; data binding. You can write values into any input in your form by changing properties on a single object. Aim small, miss small — so let’s shoot for making “it just works” the default developer experience.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/justin-schroeder/embed/xxwQymd?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Slots, custom inputs, plugins — oh my!
&lt;/h3&gt;

&lt;p&gt;This article is just an introduction — not a substitute for the full documentation — but it wouldn’t be fair to leave out some of my favorite extensibility features. Form authoring tools need to be flexible — there’s an edge case for everything right? Vue Formulate’s highly opinionated component-first API may seem at odds with flexibility, but in reality that consistent API is the core behind a highly flexible architecture.&lt;/p&gt;

&lt;p&gt;Slots are a great example of how consistency pays the bills. Central to Vue Formulate’s inputs is a &lt;a href="https://vueformulate.com/guide/inputs/slots/#context-object" rel="noopener noreferrer"&gt;comprehensive &lt;code&gt;context&lt;/code&gt; object&lt;/a&gt; that dictates virtually everything about an input. The model, validation errors, label value, help text, and lots (lots!) more are members of this object. Because every input has a consistent API, every input has a consistent context object.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/justin-schroeder/embed/rNOQQww?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;While the flexibility to use scoped slots is great — they can hurt the consistency and readability of our form’s code. To address this, Vue Formulate also includes the ability to override the default value of every slot. We call these &lt;a href="https://vueformulate.com/guide/inputs/slots/#slot-components" rel="noopener noreferrer"&gt;“Slot Components”&lt;/a&gt;, and they’re fundamental to maintaining a clean consistent authoring API. Want to add that example tooltip to every label? No problem. You can replace the default value in the label slot on every input in your project without having to use scoped slots or wrap your components in your templates at all.&lt;/p&gt;

&lt;p&gt;If you decide you’re better off creating your own custom input type, you can do that too! Custom inputs keep form authoring buttery-smooth, just pick your own input &lt;code&gt;type&lt;/code&gt; and register it with Vue Formulate. Your custom input will get validation, labels, help text, model binding, and more out of the box. Even better, once you’ve created a custom input you can easily turn it into a plugin to share with your team members or the larger community.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where you go is where I wanna be...
&lt;/h3&gt;

&lt;p&gt;In the excellent Honeypot &lt;a href="https://www.youtube.com/watch?v=OrxmtDw4pVI" rel="noopener noreferrer"&gt;Vue documentary&lt;/a&gt;, Thorsten Lünborg summed up what I consider to be the number one reasons for Vue’s spectacular success:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The focus in Vue.js from the get-go was always that the framework is more than just the code. It’s not like, “this is the library, this is the documentation, of how it works, and now you solve the rest.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In essence, the Vue core team was willing to go where developers were feeling pain points the most. As a result they have created a framework that isn’t just elegant — it’s delightful for real-world developers to use. Vue Formulate maintains this spirit; to meet developer’s pain points with delightful form authoring solutions. We believe we’ve now paved the road for 90% of users — but if your road is less traveled and you find yourself at an edge case — please shout it out. We're listening.&lt;/p&gt;




&lt;p&gt;If you’re intrigued, checkout &lt;a href="https://vueformulate.com/" rel="noopener noreferrer"&gt;vueformulate.com&lt;/a&gt;. You can follow me, &lt;a href="https://twitter.com/jpschroeder" rel="noopener noreferrer"&gt;Justin Schroeder&lt;/a&gt;, on twitter — as well as my co-maintainer &lt;a href="https://twitter.com/BoydDotDev" rel="noopener noreferrer"&gt;Andrew Boyd&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>vue</category>
      <category>forms</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
