Remove idForLabel argument from Widget and BoundWidget constructors

The primary use-case for this was to point to the first item of a radio select, and Django no longer does this as of 4.0 (b9e872b593). In all other known cases, the default behaviour of returning the one input element's ID is sufficient - and if any other widget has non-standard handling of input elements to the point that this isn't the case, then it will certainly need its own Widget subclass on the JS side, and at that point it makes far more sense to provide a suitable JS implementation of idForLabel rather than doing a string replacement on whatever the Django function gives you.

This shouldn't break any documented APIs, as the base Widget class was not exposed outside of entrypoints/admin/telepath/widgets.js on previous releases, meaning that third-party widgets are expected to implement their own work-alike classes which will be unchanged by this. We update the widget API documentation to reflect the fact that an id_for_label parameter is no longer considered a good idea, though.
pull/13248/head
Matt Westcott 2025-07-10 20:13:53 +01:00 zatwierdzone przez Sage Abdullah
rodzic 2ace1dcde1
commit 0f84deaf45
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: EB1A33CC51CC0217
4 zmienionych plików z 8 dodań i 21 usunięć

Wyświetl plik

@ -32,7 +32,7 @@ export const querySelectorIncludingSelf = (elementOrNodeList, selector) => {
};
export class BoundWidget {
constructor(elementOrNodeList, name, idForLabel, parentCapabilities) {
constructor(elementOrNodeList, name, parentCapabilities) {
// look for an input element with the given name
const selector = `:is(input,select,textarea,button)[name="${name}"]`;
this.input = querySelectorIncludingSelf(elementOrNodeList, selector);
@ -40,7 +40,7 @@ export class BoundWidget {
throw new Error(`No input found with name "${name}"`);
}
this.idForLabel = idForLabel;
this.idForLabel = this.input.id;
this.parentCapabilities = parentCapabilities || new Map();
}
@ -84,9 +84,8 @@ export class BoundWidget {
}
export class Widget {
constructor(html, idPattern) {
constructor(html) {
this.html = html;
this.idPattern = idPattern;
}
boundWidgetClass = BoundWidget;
@ -100,7 +99,6 @@ export class Widget {
options = {},
) {
const html = this.html.replace(/__NAME__/g, name).replace(/__ID__/g, id);
const idForLabel = this.idPattern.replace(/__ID__/g, id);
/* write the HTML into a temp container to parse it into a node list */
const tempContainer = document.createElement('div');
@ -128,7 +126,6 @@ export class Widget {
const boundWidget = new this.boundWidgetClass(
childElements.length === 1 ? childElements[0] : childNodes,
name,
idForLabel,
parentCapabilities,
);
boundWidget.setState(initialState);
@ -160,10 +157,10 @@ export class CheckboxInput extends Widget {
}
export class BoundRadioSelect {
constructor(element, name, idForLabel) {
constructor(element, name) {
this.element = element;
this.name = name;
this.idForLabel = idForLabel;
this.idForLabel = '';
this.isMultiple = !!this.element.querySelector(
`input[name="${name}"][type="checkbox"]`,
);

Wyświetl plik

@ -10,7 +10,6 @@ describe('Widget', () => {
widgetDef = new Widget(
'<input type="text" name="__NAME__" maxlength="255" id="__ID__">',
'__ID__',
);
boundWidget = widgetDef.render(
document.getElementById('placeholder'),
@ -29,7 +28,7 @@ describe('Widget', () => {
test('it fails rendering if no input is found', () => {
document.body.innerHTML = '<div id="placeholder"></div>';
const nonWidget = new Widget('<div>Not an input</div>', '__ID__');
const nonWidget = new Widget('<div>Not an input</div>');
expect(() => {
nonWidget.render(
document.getElementById('placeholder'),
@ -120,7 +119,6 @@ describe('Widget with inline JS', () => {
widgetDef = new Widget(
'<div><input type="text" name="__NAME__" maxlength="255" id="__ID__"><script>document.getElementById("__ID__").className = "custom-class";</script></div>',
'__ID__',
);
boundWidget = widgetDef.render(
document.getElementById('placeholder'),
@ -152,7 +150,6 @@ describe('Widget with multiple top-level nodes', () => {
widgetDef = new Widget(
'<!-- here comes a widget --><input type="text" name="__NAME__" maxlength="255" id="__ID__"><button data-button-state="idle">Click me</button><script>document.getElementById("__ID__").className = "custom-class";</script>',
'__ID__',
);
boundWidget = widgetDef.render(
document.getElementById('placeholder'),
@ -191,7 +188,6 @@ describe('RadioSelect', () => {
<input type="radio" name="__NAME__" value="coffee" id="__ID___1"> Coffee</label>
</li>
</ul>`,
'__ID___0',
);
boundWidget = widgetDef.render(
document.getElementById('placeholder'),
@ -273,7 +269,6 @@ describe('RadioSelect for CheckboxSelectMultiple', () => {
<input type="checkbox" name="__NAME__" value="blue" id="__ID___2"> Blue</label>
</li>
</ul>`,
'__ID___0',
);
boundWidget = widgetDef.render(
document.getElementById('placeholder'),
@ -315,7 +310,6 @@ describe('CheckboxInput', () => {
const widgetDef = new CheckboxInput(
'<input type="checkbox" name="__NAME__" id="__ID__">',
'__ID__',
);
boundWidget = widgetDef.render(
document.getElementById('placeholder'),
@ -365,7 +359,6 @@ describe('Select', () => {
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>`,
'__ID__',
);
boundWidget = widgetDef.render(
document.getElementById('placeholder'),
@ -417,7 +410,6 @@ describe('Select multiple', () => {
<option value="green">Green</option>
<option value="blue">Blue</option>
</select>`,
'__ID__',
);
boundWidget = widgetDef.render(
document.getElementById('placeholder'),

Wyświetl plik

@ -24,10 +24,9 @@ class FancyInputAdapter(WidgetAdapter):
def js_args(self, widget):
return [
# Arguments typically include the widget's HTML representation
# and label ID rendered with __NAME__ and __ID__ placeholders,
# for use in the client-side render() method
# rendered with __NAME__ and __ID__ placeholders, for use in the
# client-side render() method
widget.render('__NAME__', None, attrs={'id': '__ID__'}),
widget.id_for_label('__ID__'),
widget.extra_options,
]

Wyświetl plik

@ -18,7 +18,6 @@ class WidgetAdapter(Adapter):
def js_args(self, widget):
return [
widget.render("__NAME__", None, attrs={"id": "__ID__"}),
widget.id_for_label("__ID__"),
]
def get_media(self, widget):