Browse Source

Added JS Events for Inline panel added/removed

- Added docs for InlinePanel JS events
- Added w-formset:ready w-formset:added & w-formset:removed events and expanded test suite
- Fixes #9105
faishalmanzar 1 năm trước cách đây
mục cha
commit
e9d88528e7

+ 1 - 0
CHANGELOG.txt

@@ -41,6 +41,7 @@ Changelog
  * Increase the read buffer size to improve efficiency and performance when generating file hashes for document or image uploads, use `hashlib.file_digest` if available (Python 3.11+) (Jake Howard)
  * API ordering now supports multiple fields (Rohit Sharma, Jake Howard)
  * Pass block value to `Block.get_template` to allow varying template based on value (Florian Delizy)
+ * Add `InlinePanel` DOM events for when ready and when items added or removed (Faishal Manzar)
  * Fix: Ensure that StreamField's `FieldBlock`s correctly set the `required` and `aria-describedby` attributes (Storm Heg)
  * Fix: Avoid an error when the moderation panel (admin dashboard) contains both snippets and private pages (Matt Westcott)
  * Fix: When deleting collections, ensure the collection name is correctly shown in the success message (LB (Ben) Johnston)

+ 0 - 18
client/src/components/InlinePanel/__snapshots__/index.test.js.snap

@@ -1,18 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`InlinePanel should allow inserting a new form and calling an onAdd function 1`] = `
-"
-<form>
-    <input name="person_cafe_relationship-TOTAL_FORMS" value="1" id="id_person_cafe_relationship-TOTAL_FORMS" type="hidden">
-    <input name="person_cafe_relationship-INITIAL_FORMS" value="0" id="id_person_cafe_relationship-INITIAL_FORMS" type="hidden">
-    <input name="person_cafe_relationship-MIN_NUM_FORMS" value="1" id="id_person_cafe_relationship-MIN_NUM_FORMS" type="hidden">
-    <input name="person_cafe_relationship-MAX_NUM_FORMS" value="5" id="id_person_cafe_relationship-MAX_NUM_FORMS" type="hidden">
-    <div id="id_person_cafe_relationship-FORMS">
-        <p id="person_cafe_relationship-0">form for inline child</p>
-    </div>
-    <template id="id_person_cafe_relationship-EMPTY_FORM_TEMPLATE">
-        <p id="person_cafe_relationship-__prefix__">form for inline child</p>
-    </template>
-    <button type="button" id="id_person_cafe_relationship-ADD">Add item</button>
-</form>"
-`;

+ 38 - 2
client/src/components/InlinePanel/index.js

@@ -28,6 +28,16 @@ export class InlinePanel extends ExpandingFormset {
     }
 
     this.updateControlStates();
+    // dispatch event for form ready
+    setTimeout(() => {
+      this.formsElt.get(0)?.dispatchEvent(
+        new CustomEvent('w-formset:ready', {
+          bubbles: true,
+          cancelable: false,
+          detail: { ...opts },
+        }),
+      );
+    });
   }
 
   updateControlStates() {
@@ -47,9 +57,20 @@ export class InlinePanel extends ExpandingFormset {
 
     $('#' + deleteInputId + '-button').on('click', () => {
       /* set 'deleted' form field to true */
-      $('#' + deleteInputId).val('1');
+      $('#' + deleteInputId)
+        .val('1')
+        .get(0)
+        .dispatchEvent(new Event('change', { bubbles: true }));
       currentChild.addClass('deleted').slideUp(() => {
         this.updateControlStates();
+        // dispatch event for deleting form
+        currentChild.get(0).dispatchEvent(
+          new CustomEvent('w-formset:removed', {
+            bubbles: true,
+            cancelable: false,
+            detail: { ...this.opts },
+          }),
+        );
       });
     });
 
@@ -252,7 +273,10 @@ export class InlinePanel extends ExpandingFormset {
     this.initChildControls(newChildPrefix);
     if (this.opts.canOrder) {
       /* ORDER values are 1-based, so need to add 1 to formIndex */
-      $('#id_' + newChildPrefix + '-ORDER').val(formIndex + 1);
+      $('#id_' + newChildPrefix + '-ORDER')
+        .val(formIndex + 1)
+        .get(0)
+        .dispatchEvent(new Event('change', { bubbles: true }));
     }
 
     this.updateControlStates();
@@ -268,5 +292,17 @@ export class InlinePanel extends ExpandingFormset {
     }
 
     this.initialFocus($(`#inline_child_${newChildPrefix}-panel-content`));
+
+    const newChild = this.formsElt.children().last().get(0);
+    if (!newChild) return;
+
+    // dispatch event for initialising a form
+    newChild.dispatchEvent(
+      new CustomEvent('w-formset:added', {
+        bubbles: true,
+        cancelable: false,
+        detail: { formIndex, ...this.opts },
+      }),
+    );
   }
 }

+ 73 - 10
client/src/components/InlinePanel/index.test.js

@@ -1,39 +1,102 @@
+import $ from 'jquery';
+
+import { InlinePanel } from './index';
+
+jest.useFakeTimers();
+
 describe('InlinePanel', () => {
-  let InlinePanel;
+  const handleAddedEvent = jest.fn();
+  const handleRemovedEvent = jest.fn();
+  const handleReadyEvent = jest.fn();
+
+  const onAdd = jest.fn();
 
   beforeAll(() => {
-    InlinePanel = require('./index').InlinePanel;
+    $.fx.off = true;
+    jest.resetAllMocks();
 
     document.body.innerHTML = `
-<form>
+  <form>
     <input name="person_cafe_relationship-TOTAL_FORMS" value="0" id="id_person_cafe_relationship-TOTAL_FORMS" type="hidden" />
     <input name="person_cafe_relationship-INITIAL_FORMS" value="0" id="id_person_cafe_relationship-INITIAL_FORMS" type="hidden" />
     <input name="person_cafe_relationship-MIN_NUM_FORMS" value="1" id="id_person_cafe_relationship-MIN_NUM_FORMS" type="hidden" />
     <input name="person_cafe_relationship-MAX_NUM_FORMS" value="5" id="id_person_cafe_relationship-MAX_NUM_FORMS" type="hidden" />
     <div id="id_person_cafe_relationship-FORMS"></div>
     <template id="id_person_cafe_relationship-EMPTY_FORM_TEMPLATE">
-        <p id="person_cafe_relationship-__prefix__">form for inline child</p>
+      <div id="inline_child_person_cafe_relationship-__prefix__" data-child-form-mock>
+        <p>Form for inline child</div>
+        <button type="button" id="id_person_cafe_relationship-__prefix__-DELETE-button">Delete</button>
+        <input type="hidden" name="id_person_cafe_relationship-__prefix__-DELETE" id="id_person_cafe_relationship-__prefix__-DELETE">
+      </div>
     </template>
     <button type="button" id="id_person_cafe_relationship-ADD">Add item</button>
-</form>`;
+  </form>`;
+
+    document.addEventListener('w-formset:added', handleAddedEvent);
+    document.addEventListener('w-formset:ready', handleReadyEvent);
+    document.addEventListener('w-formset:removed', handleRemovedEvent);
   });
 
-  const onAdd = jest.fn();
+  it('tests inline panel `w-formset:ready` event', () => {
+    expect(handleReadyEvent).not.toHaveBeenCalled();
 
-  it('should allow inserting a new form and calling an onAdd function', () => {
     const options = {
-      formsetPrefix: 'id_person_cafe_relationship',
       emptyChildFormPrefix: 'person_cafe_relationship-__prefix__',
-      onAdd: onAdd,
+      formsetPrefix: 'id_person_cafe_relationship',
+      onAdd,
     };
 
     new InlinePanel(options);
 
+    jest.runAllTimers();
+
+    expect(handleReadyEvent).toHaveBeenCalled();
+  });
+
+  it('should allow inserting a new form and also dispatches `w-formset:added` event on calling onAdd function', () => {
+    expect(handleAddedEvent).not.toHaveBeenCalled();
     expect(onAdd).not.toHaveBeenCalled();
+    expect(document.querySelectorAll('[data-child-form-mock]')).toHaveLength(0);
 
     // click the 'add' button
     document.getElementById('id_person_cafe_relationship-ADD').click();
+
+    expect(document.querySelectorAll('[data-child-form-mock]')).toHaveLength(1);
     expect(onAdd).toHaveBeenCalled();
-    expect(document.body.innerHTML).toMatchSnapshot();
+
+    document.getElementById('id_person_cafe_relationship-ADD').click();
+    expect(onAdd).toHaveBeenCalledTimes(2);
+    expect(document.querySelectorAll('[data-child-form-mock]')).toHaveLength(2);
+
+    // check events were dispatched
+    expect(handleAddedEvent).toHaveBeenCalledTimes(2);
+    const [event] = handleAddedEvent.mock.calls[0];
+
+    expect(event.bubbles).toEqual(true);
+    expect(event.detail).toMatchObject({
+      formIndex: 0,
+      formsetPrefix: 'id_person_cafe_relationship',
+      emptyChildFormPrefix: 'person_cafe_relationship-__prefix__',
+    });
+  });
+
+  it('should allow removing a form', async () => {
+    expect(handleRemovedEvent).not.toHaveBeenCalled();
+    expect(document.querySelectorAll('[data-child-form-mock]')).toHaveLength(2);
+    expect(
+      document.querySelectorAll('.deleted[data-child-form-mock]'),
+    ).toHaveLength(0);
+
+    // click the 'delete' button
+    document
+      .getElementById('id_person_cafe_relationship-0-DELETE-button')
+      .click();
+
+    expect(document.querySelectorAll('[data-child-form-mock]')).toHaveLength(2);
+    expect(
+      document.querySelectorAll('.deleted[data-child-form-mock]'),
+    ).toHaveLength(1);
+
+    expect(handleRemovedEvent).toHaveBeenCalledTimes(1);
   });
 });

+ 44 - 0
docs/reference/pages/panels.md

@@ -109,6 +109,50 @@ Here are some built-in panel types that you can use in your panel definitions. T
 
 ```
 
+(inline_panel_events)=
+
+#### JavaScript DOM events
+
+You may want to execute some JavaScript when `InlinePanel` items are ready, added or removed. The `w-formset:ready`, `w-formset:added` and `w-formset:removed` events allow this.
+
+For example, given a child model that provides a relationship between Blog and Person on `BlogPage`.
+
+```python
+class CustomInlinePanel(InlinePanel):
+    class BoundPanel(InlinePanel.BoundPanel):
+        class Media:
+            js = ["js/inline-panel.js"]
+
+
+class BlogPage(Page):
+        # .. fields
+
+        content_panels = Page.content_panels + [
+               CustomInlinePanel("blog_person_relationship"),
+              # ... other panels
+        ]
+```
+
+Using the JavaScript as follows.
+
+```javascript
+// static/js/inline-panel.js
+
+document.addEventListener('w-formset:ready', function (event) {
+    console.info('ready', event);
+});
+
+document.addEventListener('w-formset:added', function (event) {
+    console.info('added', event);
+});
+
+document.addEventListener('w-formset:removed', function (event) {
+    console.info('removed', event);
+});
+```
+
+Events will be dispatched and can trigger custom JavaScript logic such as setting up a custom widget.
+
 (multiple_chooser_panel)=
 
 ### MultipleChooserPanel

+ 1 - 0
docs/releases/5.2.md

@@ -53,6 +53,7 @@ depth: 1
  * Increase the read buffer size to improve efficiency and performance when generating file hashes for document or image uploads, use `hashlib.file_digest` if available (Python 3.11+) (Jake Howard)
  * API ordering now [supports multiple fields](api_v2_usage_ordering) (Rohit Sharma, Jake Howard)
  * Pass block value to `Block.get_template` to allow varying template based on value (Florian Delizy)
+ * Add [`InlinePanel` DOM events](inline_panel_events) for when ready and when items added or removed (Faishal Manzar)
 
 ### Bug fixes