Repository: django/djangoproject.com
Pull request #1987: Refactored `fundraising-index.js`
Author: login: adamzap
Created: 2025-03-14T22:18:21Z | Merged: 2025-03-25T15:35:53Z

- Stopped using jQuery
- Moved refactored code to `djangoproject.js`
- Attached only one event handler per `select` element
- Simplified custom amount input focussing

---

Thankfully this code around donations was pretty straightforward. To test, just compare the functionality of changing these `select` elements with production:

<img width="334" alt="image" src="https://github.com/user-attachments/assets/37692535-2e57-4f7e-a4e2-55c9b48c99ea" />

I wasn't able to test the Stripe integration, but it may be wise for someone with access to the sandbox to do it just in case.


Diff:
diff --git a/djangoproject/static/js/djangoproject.js b/djangoproject/static/js/djangoproject.js
index 3515682e31..eeabc37362 100644
--- a/djangoproject/static/js/djangoproject.js
+++ b/djangoproject/static/js/djangoproject.js
@@ -183,3 +183,36 @@ document.querySelectorAll('.btn-clipboard').forEach(function (el) {
 
   window.addEventListener('hashchange', scroll_to_hash);
 })();
+
+// Update donate button text on fundraising page based on interval selection
+document
+  .querySelector('#donate #id_interval')
+  .addEventListener('change', function () {
+    const text = this.value === 'onetime' ? 'Donate' : `Donate ${this.value}`;
+
+    document.getElementById('donate-button').value = text;
+  });
+
+// Manage custom donation amount input on fundraising page
+document
+  .querySelector('#donate #id_amount')
+  .addEventListener('change', function () {
+    if (this.value !== 'custom') {
+      return;
+    }
+
+    const input_el = document.createElement('input');
+
+    input_el.setAttribute('type', 'number');
+    input_el.setAttribute('name', 'amount');
+
+    const custom_donation_el = document.querySelector('.custom-donation');
+
+    custom_donation_el.appendChild(input_el);
+    custom_donation_el.style.display = 'block';
+
+    this.remove();
+
+    input_el.focus();
+    input_el.value = '25';
+  });
diff --git a/djangoproject/static/js/main.js b/djangoproject/static/js/main.js
index fdc284c6b3..d076ddb6da 100644
--- a/djangoproject/static/js/main.js
+++ b/djangoproject/static/js/main.js
@@ -16,10 +16,6 @@ define(function () {
   if (hasClass('fundraising-heart')) {
     mods.push('mod/fundraising-heart');
   }
-  //fundraising donation form
-  if (hasClass('fundraising-index')) {
-    mods.push('mod/fundraising-index');
-  }
 
   if (hasClass('dashboard-index')) {
     mods.push('dashboard/index');
diff --git a/djangoproject/static/js/mod/fundraising-index.js b/djangoproject/static/js/mod/fundraising-index.js
deleted file mode 100644
index 6a0254cff9..0000000000
--- a/djangoproject/static/js/mod/fundraising-index.js
+++ /dev/null
@@ -1,46 +0,0 @@
-define([
-  'jquery', //requires jquery
-], function ($) {
-  var FundraisingIndex = function (form) {
-    this.form = $(form); // the floating warning div
-    this.init();
-  };
-
-  FundraisingIndex.prototype = {
-    init: function () {
-      var self = this;
-      $(document).ready(function () {
-        self.form.find('select').on('change', self.setDonation);
-        self.form
-          .find('select')
-          .on('change', self.setDonateButtonText)
-          .change();
-      });
-    },
-    setDonation: function (event) {
-      if ($(this).val() == 'custom') {
-        $(this).remove();
-        $('.custom-donation')
-          .append('<input type="number" name="amount" value="25">')
-          .show();
-        var input = $('.custom-donation input');
-        input.focus();
-        // here we're moving the "focus" at the end of the input text
-        var tmpStr = input.val();
-        input.val('');
-        input.val(tmpStr);
-      }
-    },
-    setDonateButtonText: function (event) {
-      var text = 'Donate';
-      var interval = $('#id_interval').val();
-      if (interval != 'onetime') {
-        text += ' ' + interval;
-      }
-      $('#donate-button').val(text);
-    },
-  };
-
-  // Export a single instance of our module:
-  return new FundraisingIndex('.fundraising-index form');
-});


Reviews:
user: login: pauloxnet
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-15T13:53:31Z
user: login: pauloxnet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-16T15:45:35Z
user: login: adamzap
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T02:38:22Z
user: login: pauloxnet
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-25T15:35:24Z

Review comments:
diff_hunk: @@ -183,3 +183,36 @@ document.querySelectorAll('.btn-clipboard').forEach(function (el) {
 
   window.addEventListener('hashchange', scroll_to_hash);
 })();
+
+// Update donate button text on fundraising page based on interval selection
+document
+  .querySelector('#donate #id_interval')
+  .addEventListener('change', function () {
+    const text = this.value === 'onetime' ? 'Donate' : `Donate ${this.value}`;
+
+    document.getElementById('donate-button').value = text;
+  });
+
+// Manage custom donation amount input on fundraising page
+document
+  .querySelector('#donate #id_amount')
+  .addEventListener('change', function () {
+    if (this.value !== 'custom') {
+      return;
+    }
+
+    const input_el = document.createElement('input');
+
+    input_el.setAttribute('type', 'number');
+    input_el.setAttribute('name', 'amount');
+
+    const custom_donation_el = document.querySelector('.custom-donation');
+
+    custom_donation_el.appendChild(input_el);
+    custom_donation_el.style.display = 'block';
+
+    this.remove();
+
+    input_el.focus();
+    input_el.value = '25';
+  });
path: djangoproject/static/js/djangoproject.js
user: login: pauloxnet
body: ```suggestion
document.addEventListener('DOMContentLoaded', () => {
  // Update donate button text on fundraising page based on interval selection
  const intervalSelect = document.querySelector('#donate #id_interval');
  intervalSelect?.addEventListener('change', ({ target }) => {
    const text = target.value === 'onetime' ? 'Donate' : `Donate ${target.value}`;
    document.getElementById('donate-button').value = text;
  });

  // Manage custom donation amount input on fundraising page
  const amountSelect = document.querySelector('#donate #id_amount');
  amountSelect?.addEventListener('change', ({ target }) => {
    if (target.value !== 'custom') return;

    const inputEl = document.createElement('input');
    inputEl.type = 'number';
    inputEl.name = 'amount';
    inputEl.value = '25';

    const customDonationEl = document.querySelector('.custom-donation');
    customDonationEl.appendChild(inputEl);
    customDonationEl.style.display = 'block';

    target.remove();
    inputEl.focus();
  });
});
```

This is only an experiment I did with copilot, but I'm curious about your opinions on these suggestions. Feel free to skip everything.

1. Event Delegation and DOMContentLoaded:

    * Wrapping the code inside DOMContentLoaded ensures the DOM is fully loaded before attaching event listeners, preventing potential errors if the elements are not yet available.
    * Benefit: Improves reliability by ensuring elements are present before accessing them.

2. Optional Chaining (?.):

    * Using optional chaining (?.) to safely attempt to add event listeners without throwing errors if elements are not found.
    * Benefit: Prevents runtime errors and improves code robustness.

3. Destructuring Event Objects:

    * Using destructuring in the event handler parameter to directly access target.
    * Benefit: Cleaner and more readable code by reducing repetitive access to event.target.

4. Modern Element Creation and Attribute Setting:
    * Using property assignment instead of setAttribute for setting attributes on the newly created input element.
    * Benefit: More concise and readable code.

5. Early Return:
    * Utilizing early return for conditions to reduce nesting and improve readability.
    * Benefit: Makes the code logic easier to follow and understand.

6. Consistent Naming Conventions:
    * Using camelCase for variable names to follow JavaScript naming conventions.
    * Benefit: Consistency in naming improves readability and maintainability of the code.

By implementing these improvements, the code will be more modern, readable, and maintainable. It will also be less prone to runtime errors and easier to debug and extend in the future.

created_at: 2025-03-16T15:45:23Z
author_association: MEMBER
diff_hunk: @@ -183,3 +183,36 @@ document.querySelectorAll('.btn-clipboard').forEach(function (el) {
 
   window.addEventListener('hashchange', scroll_to_hash);
 })();
+
+// Update donate button text on fundraising page based on interval selection
+document
+  .querySelector('#donate #id_interval')
+  .addEventListener('change', function () {
+    const text = this.value === 'onetime' ? 'Donate' : `Donate ${this.value}`;
+
+    document.getElementById('donate-button').value = text;
+  });
+
+// Manage custom donation amount input on fundraising page
+document
+  .querySelector('#donate #id_amount')
+  .addEventListener('change', function () {
+    if (this.value !== 'custom') {
+      return;
+    }
+
+    const input_el = document.createElement('input');
+
+    input_el.setAttribute('type', 'number');
+    input_el.setAttribute('name', 'amount');
+
+    const custom_donation_el = document.querySelector('.custom-donation');
+
+    custom_donation_el.appendChild(input_el);
+    custom_donation_el.style.display = 'block';
+
+    this.remove();
+
+    input_el.focus();
+    input_el.value = '25';
+  });
path: djangoproject/static/js/djangoproject.js
user: login: adamzap
body: Interesting!

1. This shouldn't matter because the JavaScript file is included at the bottom of the page.
2. This shouldn't matter because we only attach event handlers to elements that are already found on the page. I'd rather see an error for this case than hide it.
3. I'm not sure what this means.
4. I could do this in a separate PR that affects the whole file. I didn't realize that certain attributes are properties on the element object. It seems odd to me that the web API provides two ways to do this.
5. I'm pretty sure that I'm already using early `return`s everywhere possible.
6. For this file, I've chosen to go against the typical variable name casing for JavaScript and do it the Python way instead. If there is strong disagreement on this, I can convert the file to camel case.

In summary, the fourth point may lead to a PR after this one is merged.
created_at: 2025-03-17T02:38:22Z
author_association: MEMBER

PR comments:
user: login: adamzap
created_at: 2025-03-25T15:50:12Z
body: @pauloxnet Thanks! Let me know if you want me to squash my commits before you merge.
author_association: MEMBER
user: login: pauloxnet
created_at: 2025-03-25T15:53:12Z
body: > @pauloxnet Thanks! Let me know if you want me to squash my commits before you merge.

Yes, thanks. I reviewed again the PR, but I forgot to check the number of commit.
Next time I'll remember to check it.

BTW thanks for all your PRs. :smiley: 
author_association: MEMBER
user: login: adamzap
created_at: 2025-03-25T16:00:38Z
body: No worries, thanks!
author_association: MEMBER