Dev.to WebDev 🛠 Dev 👁 0 📖 7 min read

A category filter should not reset an edited textarea

Imagine a page with two generated messages. You edit one, switch the category selector to hide it, and then switch back. The edited message should still be there. This goes wrong when the category handler calls the same

Imagine a page with two generated messages. You edit one, switch the category selector to hide it, and then switch back. The edited message should still be there.

This goes wrong when the category handler calls the same function that generates the starting text. That function may filter the cards correctly while also assigning new values to every textarea.

The fix begins with three separate concepts: the original template, the values used to personalize it, and the text a person has edited. Filtering changes visibility. Regeneration writes new draft text. Copy and download read the current draft.

Try the complete example

Save the following as example.html and open it in a browser. The messages and group names are fictional. No account, framework, network service, or build step is needed. Automatic copying depends on the browser allowing clipboard access; the example selects the text for manual copying if that write fails. MDN documents the clipboard security requirements.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Editable templates with a category filter</title>
  <style>
    * { box-sizing: border-box; }
    [hidden] { display: none !important; }
    body { margin: 0; background: #f5f7f6; color: #152f26; font: 17px/1.6 system-ui, sans-serif; }
    main { max-width: 900px; margin: auto; padding: 40px 20px; }
    h1, h2 { line-height: 1.2; }
    h1 { font-size: clamp(28px, 5vw, 42px); margin-top: 0; }
    h2 { font-size: 23px; }
    p { max-width: 68ch; }
    .panel { background: white; border: 1px solid #cbd9d2; border-radius: 12px; padding: 22px; margin: 22px 0; }
    .fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); gap: 18px; }
    label { display: block; font-weight: 650; }
    input, select, textarea, button { font: inherit; }
    input, select, textarea { display: block; width: 100%; border: 1px solid #92a99d; border-radius: 6px; padding: 10px; background: #fff; color: #152f26; }
    textarea { min-height: 140px; resize: vertical; margin: 8px 0; }
    button { border: 1px solid #235843; background: #235843; color: white; border-radius: 6px; padding: 9px 16px; cursor: pointer; }
    button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 3px solid #dc9d19; outline-offset: 3px; }
    small { display: block; color: #486557; }
    #status { min-height: 2em; padding: 12px; border-left: 4px solid #235843; }
    .notice { font-size: 15px; }
  </style>
</head>
<body>
  <main>
    <h1>Editable templates with a category filter</h1>
    <p>Edit a draft, hide it with the category filter, then show it again. Your text stays intact. Regeneration replaces edits only when you click the button.</p>
    <p class="notice">This fictional example runs in your browser. It sends no messages, uses no analytics or storage, and loses its drafts when you reload or leave. Copy or download anything you want to keep.</p>

    <section class="panel" aria-labelledby="settings-title">
      <h2 id="settings-title">Template settings</h2>
      <div class="fields">
        <label>Group name<input id="group" value="Community team" maxlength="80"></label>
        <label>Meeting time<input id="time" value="Thursday at 6pm" maxlength="80"></label>
        <label>Category<select id="category"><option value="all">All categories</option><option value="events">Events</option><option value="volunteers">Volunteers</option></select></label>
      </div>
      <p id="regeneration-note" class="notice">Regenerate replaces manual edits in every draft, including hidden drafts. Changing the name or time alone leaves the current drafts intact.</p>
      <button id="regenerate" type="button" aria-describedby="regeneration-note">Regenerate drafts</button>
    </section>

    <p id="status" role="status" aria-live="polite">Example drafts are ready.</p>
    <section aria-label="Editable drafts">
      <article class="panel" data-id="event-reminder" data-category="events">
        <h2>Event reminder</h2>
        <label for="event-message">Your event draft</label>
        <textarea id="event-message" spellcheck="true"></textarea>
        <small data-count></small>
        <button type="button" data-copy>Copy event draft</button>
      </article>
      <article class="panel" data-id="volunteer-note" data-category="volunteers">
        <h2>Volunteer note</h2>
        <label for="volunteer-message">Your volunteer draft</label>
        <textarea id="volunteer-message" spellcheck="true"></textarea>
        <small data-count></small>
        <button type="button" data-copy>Copy volunteer draft</button>
      </article>
    </section>
    <button id="download" type="button">Download visible drafts</button>
  </main>
  <script>
    const templates = [
      { id: 'event-reminder', category: 'events', title: 'Event reminder', build: ({ group, time }) => `Hi ${group}, our meetup is ${time}. Please bring a notebook.` },
      { id: 'volunteer-note', category: 'volunteers', title: 'Volunteer note', build: ({ group, time }) => `Hi ${group}, can you help at our meetup ${time}? Reply with the task you can take.` },
    ];
    const group = document.querySelector('#group');
    const time = document.querySelector('#time');
    const category = document.querySelector('#category');
    const status = document.querySelector('#status');
    const cards = new Map(templates.map((template) => [template.id, document.querySelector(`[data-id="${template.id}"]`)]));

    function updateCount(card) {
      card.querySelector('[data-count]').textContent = `${Array.from(card.querySelector('textarea').value).length} characters`;
    }

    function filterCards() {
      for (const template of templates) {
        const card = cards.get(template.id);
        card.hidden = category.value !== 'all' && template.category !== category.value;
      }
    }

    function regenerate() {
      const values = { group: group.value.trim() || 'Community team', time: time.value.trim() || 'Thursday at 6pm' };
      for (const template of templates) {
        const card = cards.get(template.id);
        card.querySelector('textarea').value = template.build(values);
        updateCount(card);
      }
      filterCards();
    }

    function visibleDrafts() {
      return templates.filter((template) => !cards.get(template.id).hidden).map((template) => ({
        title: template.title,
        message: cards.get(template.id).querySelector('textarea').value,
      }));
    }

    category.addEventListener('change', () => {
      filterCards();
      status.textContent = 'Category changed. Your draft edits are kept.';
    });
    for (const input of [group, time]) {
      input.addEventListener('input', () => {
        status.textContent = 'New details are ready. Click Regenerate drafts to replace the current text.';
      });
    }
    document.querySelector('#regenerate').addEventListener('click', () => {
      regenerate();
      status.textContent = 'Drafts regenerated. Previous edits have been replaced, including edits in hidden drafts.';
    });
    for (const card of cards.values()) {
      const textarea = card.querySelector('textarea');
      textarea.addEventListener('input', () => updateCount(card));
      card.querySelector('[data-copy]').addEventListener('click', async () => {
        try {
          await navigator.clipboard.writeText(textarea.value);
          status.textContent = 'Current draft copied.';
        } catch {
          textarea.focus();
          textarea.select();
          status.textContent = 'Automatic copying is unavailable. The draft is selected for manual copying.';
        }
      });
    }
    document.querySelector('#download').addEventListener('click', () => {
      const text = visibleDrafts().map((draft) => `${draft.title}\n\n${draft.message}`).join('\n\n---\n\n') + '\n';
      const url = URL.createObjectURL(new Blob([text], { type: 'text/plain;charset=utf-8' }));
      const link = document.createElement('a');
      link.href = url;
      link.download = 'current-drafts.txt';
      document.body.append(link);
      link.click();
      link.remove();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
      status.textContent = 'Download prepared from the current visible drafts.';
    });
    regenerate();
  </script>
</body>
</html>

Keep the category handler small

filterCards() only assigns card.hidden. It never assigns textarea.value, removes a card, or generates a replacement message. Each card keeps its stable data-id, so hiding it and showing it again retains the existing control and edited text. The hidden attribute controls rendering.

regenerate() has a separate button and a warning. That is the operation allowed to replace manual edits. It updates hidden cards too, so a draft does not quietly retain old personalization settings when it returns.

Read the text the person actually edited

The copy handler reads textarea.value at click time. The download handler reads the same current values and includes only visible cards. Neither regenerates the text before exporting it. The textarea value property represents its current contents.

The displayed count uses Unicode code points through Array.from(text).length. It is not a count of grapheme clusters, SMS segments, or billing units. JavaScript string iteration yields code points.

Check more than the initial render

Try these sequences:

  1. Edit the event draft, filter to Volunteers, and return to Events. Your edit should remain.
  2. Change the group name and click Regenerate drafts. The fresh template should replace the edit.
  3. Edit again and click Copy event draft. Paste the result and compare it with the textarea.
  4. Download visible drafts while Events is selected. The file should contain your final event text and exclude the volunteer draft.

The example keeps drafts only in the current page. Reloading restores the fictional defaults. If your interface removes controls or persists drafts, keep the edited text in state keyed by the same stable identifiers.

Disclosure: AI assisted the wording and code example.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.