jQuery 4 (legacy code and migration to DOM APIs) Module 2 – Selecting, reading and changing the page
attr(), prop(), classes and values in jQuery 4
Why checkboxes need .prop(), what .attr() returns and stores for boolean attributes in jQuery 4, and how attr(name, false) and toggleClass() changed.
What you will learn
- Choose between .attr(), .prop() and .val() for checked, selected, disabled and value
- Predict what .attr() returns and stores for boolean attributes in jQuery 3 and 4, and fix code that relies on it
- Find code that relies on .attr(name, false) or on toggleClass() without a class name, and rewrite it for jQuery 4
- Use jQuery Migrate to report these changes, and translate the code to classList, toggleAttribute() and properties
Before you start
What you’ll learn · Before you start
What you will learn
- Choose between .attr(), .prop() and .val() for checked, selected, disabled and value
- Predict what .attr() returns and stores for boolean attributes in jQuery 3 and 4, and fix code that relies on it
- Find code that relies on .attr(name, false) or on toggleClass() without a class name, and rewrite it for jQuery 4
- Use jQuery Migrate to report these changes, and translate the code to classList, toggleAttribute() and properties
Before you start
On this page
On jQuery 3.7.1, $('#news').attr('checked') returns "checked" for <input id="news" checked>; on jQuery 4.0.0 it
returns "", so if ($('#news').attr('checked')) quietly stops running. Four attribute and class calls changed
meaning in jQuery 4, none of them with an error, and all four hide in settings pages and form code of every age.
The settings form of this lesson runs in jsdom on both versions, with and without jQuery Migrate, and each run logs
what every call returned or stored. You will keep the HTML’s defaults apart from the live state of a form, fix code
that reads or writes boolean attributes with .attr(), find the silent changes to .attr(name, false) and
toggleClass(), let Migrate point at every affected line, and write the native code that says what it means.
Attributes hold the HTML defaults; properties hold the live state
An attribute is what the HTML says: checked, value="411001", selected. A
DOM property: A value on the JavaScript object of an element, such as el.checked or el.value. The browser sets it from the HTML attribute when the page loads; after that, clicks and typing change the property and leave the attribute as the default. is what the form holds now: el.checked, el.value. When the page loads, the browser sets each property from its
attribute; from then on, a click or a keystroke changes the property alone. jQuery’s documentation says the most
important thing to remember about the checked attribute is that “it does not correspond to the checked property”
(.prop()). Step through the newsletter box of the settings form:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The page loads. The HTML says checked, so the box has a checked attribute with an empty value, and its checked property starts as true.
- The user unticks it. A click changes only the property, which is now false. The attribute still says checked: it is the default, not the state.
- Reading the attribute. .attr('checked') reads the attribute, so it does not see the click. jQuery 4 returns its real value, an empty string; jQuery 3 returned "checked".
- Reading the property. .prop('checked') and the DOM's el.checked read the property, so both say false, as the user sees it.
- The attribute is the default. form.reset() sets the property back from the attribute: the box is ticked again. The property el.defaultChecked is the attribute as a boolean.
The form’s script reads the same box, the select, the PIN field and the Save button before and after changes like a user’s:
prefs/state.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Notification settings</title>
</head>
<body>
<form id="prefs">
<label><input type="checkbox" id="news" name="news" checked> Newsletter</label>
<label><input type="checkbox" id="sms" name="sms" checked="CHECKED"> SMS alerts</label>
<label><input type="checkbox" id="calls" name="calls"> Phone calls</label>
<select id="lang" name="lang">
<option value="en">English</option>
<option value="hi" selected>Hindi</option>
<option value="ta">Tamil</option>
</select>
<input type="text" id="pin" name="pin" value="411001">
<button id="save" type="submit" disabled>Save</button>
</form>
<a id="help" href="/help/" title="Help with settings">Help</a>
<div id="panel" class="card open" aria-hidden="true" data-mode="edit">Your settings</div>
<script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
<script src="state.js"></script>
</body>
</html> prefs/state.js
// state.js: what the HTML says (attributes) and what the form holds now (properties), before and after the user
// changes it. click() changes a checkbox as a user's click does.
const news = document.getElementById('news');
const calls = document.getElementById('calls');
const q = (value) => JSON.stringify(value);
console.log(`news on load: attribute ${q(news.getAttribute('checked'))}, .prop('checked') ${$(news).prop('checked')}`);
const callsValueUnticked = $('#calls').val();
news.click();
calls.click();
console.log(`news after a click: attribute ${q(news.getAttribute('checked'))}, .prop('checked') ${$(news).prop('checked')}, el.checked ${news.checked}`);
console.log(`calls after a click: attribute ${q(calls.getAttribute('checked'))}, .prop('checked') ${$(calls).prop('checked')}`);
console.log(`news.defaultChecked (the attribute as a boolean): ${news.defaultChecked}`);
console.log(`$('#calls').val(), a box with no value attribute: ${q(callsValueUnticked)} unticked, ${q($('#calls').val())} ticked`);
$('#lang').val('ta');
const hindi = $('#lang option[value=hi]');
console.log(`select after .val('ta'): .val() ${q($('#lang').val())}, Hindi .prop('selected') ${hindi.prop('selected')}, Hindi still has the attribute: ${hindi[0].hasAttribute('selected')}`);
document.getElementById('pin').value = '560001';
console.log(`pin after typing: .val() ${q($('#pin').val())}, .attr('value') ${q($('#pin').attr('value'))}, .prop('defaultValue') ${q($('#pin').prop('defaultValue'))}`);
$('#save').prop('disabled', false);
console.log(`save after .prop('disabled', false): the disabled attribute is gone: ${!document.getElementById('save').hasAttribute('disabled')}`);
document.getElementById('prefs').reset();
console.log(`after form.reset(): news ${news.checked}, calls ${calls.checked}, pin ${q($('#pin').val())}, language ${q($('#lang').val())}`); // Prints what prefs/state.js logged in jsdom (recorded.mjs).
import { show } from './show.mjs';
show('prefs/state.html'); Output
news on load: attribute "", .prop('checked') true
news after a click: attribute "", .prop('checked') false, el.checked false
calls after a click: attribute null, .prop('checked') true
news.defaultChecked (the attribute as a boolean): true
$('#calls').val(), a box with no value attribute: "on" unticked, "on" ticked
select after .val('ta'): .val() "ta", Hindi .prop('selected') false, Hindi still has the attribute: true
pin after typing: .val() "560001", .attr('value') "411001", .prop('defaultValue') "411001"
save after .prop('disabled', false): the disabled attribute is gone: true
after form.reset(): news true, calls false, pin "411001", language "hi"
(jQuery 3.7.1 logged the same 9 lines)
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node state.mjs
Every pair tells the same story: after the click the checked attribute was still there and .prop('checked') said
false; after typing, .attr('value') still said 411001 and .val() said 560001. The attribute is the default, which is
also why form.reset() brought every field back to it. Two lines catch people out: .val() of a checkbox is the
value it sends, "on" when the HTML gives none, unticked or ticked, never whether it is ticked; and
.prop('disabled', false) removed the disabled attribute too, because for disabled the property and the attribute
are kept in step, unlike checked. The table sums up which call answers which question:
| You want | jQuery | DOM |
|---|---|---|
| Is it ticked, selected, disabled now? | .prop('checked') |
el.checked |
| What it holds now | .val() |
el.value |
| The default from the HTML | .attr('value'), .attr('checked') !== undefined |
el.defaultValue, el.defaultChecked |
Checkpoint
Check: Attributes and properties
jQuery 4 returns boolean attributes as the HTML writes them
checked, selected and disabled are boolean attribute: An HTML attribute such as checked, selected or disabled whose presence means true and absence false. Its only valid values are an empty string and its own name; true and false are not allowed.: their presence
means true, their absence false, and the HTML Standard allows only an empty string or the attribute’s own name as a
value (HTML Standard). From
1.6 to 3.x, .attr() papered over this: it returned the name whatever the HTML said, and stored the name whatever it
was given. jQuery 4 returns and stores the real value (.attr()):
// booleans.js: .attr() on boolean attributes. jQuery 4 returns and stores what the attribute really holds.
const q = (value) => JSON.stringify(value);
const box = (id) => document.getElementById(id);
console.log(`.attr('checked'): news ${q($('#news').attr('checked'))}, sms ${q($('#sms').attr('checked'))}, calls ${q($('#calls').attr('checked'))}`);
console.log(`if ($('#news').attr('checked')): ${$('#news').attr('checked') ? 'runs' : 'skipped'}`);
console.log(`.attr('disabled') of save: ${q($('#save').attr('disabled'))}`);
console.log(`.attr('selected') of Hindi: ${q($('#lang option[value=hi]').attr('selected'))}`);
$('#calls').attr('checked', 'yes');
console.log(`.attr('checked', 'yes') stores ${q(box('calls').getAttribute('checked'))}`);
$('#calls').attr('checked', true);
console.log(`.attr('checked', true) stores ${q(box('calls').getAttribute('checked'))}`);
$('#calls').attr('checked', false);
console.log(`.attr('checked', false) leaves the attribute: ${box('calls').hasAttribute('checked')}`);
console.log(`.prop('checked') of news, either way: ${$('#news').prop('checked')}`); // Prints what prefs/booleans.js logged in jsdom (recorded.mjs).
import { show } from './show.mjs';
show('prefs/booleans.html'); Output
.attr('checked'): news "", sms "CHECKED", calls undefined
if ($('#news').attr('checked')): skipped
.attr('disabled') of save: ""
.attr('selected') of Hindi: ""
.attr('checked', 'yes') stores "yes"
.attr('checked', true) stores "true"
.attr('checked', false) leaves the attribute: false
.prop('checked') of news, either way: true
(jQuery 3.7.1 logged other lines:)
line 1: .attr('checked'): news "checked", sms "checked", calls undefined
line 2: if ($('#news').attr('checked')): runs
line 3: .attr('disabled') of save: "disabled"
line 4: .attr('selected') of Hindi: "selected"
line 5: .attr('checked', 'yes') stores "checked"
line 6: .attr('checked', true) stores "checked"
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node booleans.mjs
| Call | jQuery 3.7.1 | jQuery 4.0.0 |
|---|---|---|
.attr('checked') on <input checked> |
"checked" |
"" |
.attr('checked') on checked="CHECKED" |
"checked" |
"CHECKED" |
if ($box.attr('checked')) |
runs | skipped |
.attr('checked', 'yes') stores |
"checked" |
"yes" |
.attr('checked', true) stores |
"checked" |
"true" |
The empty string is the dangerous line: it is falsy, so every if that tested the getter now takes the other branch.
Neither version’s getter ever said whether the box was ticked: the .prop('checked') line of the output is the
call that does, on both versions.
Testing a box with the attribute getter
Wrong: if ($('#news').attr('checked')), which is false for <input checked> on jQuery 4 and ignores the user’s
clicks on every version. Right: if ($('#news').prop('checked')) for the state, or el.hasAttribute('checked')
when you really mean the HTML’s default.
Checkpoint
Check: Boolean attributes in jQuery 4
.attr(name, false) now removes the attribute
In jQuery 3, passing false to .attr() removed boolean attributes and stored the text "false" in every other one.
In jQuery 4, false removes any attribute whose name does not start with aria-; an ARIA attribute still gets the
text "false", because for aria-hidden and its relatives “false” is a value with a meaning
(.attr()):
// attr-false.js: .attr(name, false) on attributes that are not boolean.
const q = (value) => JSON.stringify(value);
const state = (el, name) => (el.hasAttribute(name) ? `kept as ${q(el.getAttribute(name))}` : 'removed');
const help = document.getElementById('help');
const panel = document.getElementById('panel');
$('#help').attr('title', false);
console.log(`.attr('title', false): title ${state(help, 'title')}`);
$('#panel').attr('data-mode', false);
console.log(`.attr('data-mode', false): data-mode ${state(panel, 'data-mode')}`);
$('#panel').attr('aria-hidden', false);
console.log(`.attr('aria-hidden', false): aria-hidden ${state(panel, 'aria-hidden')}`);
$('#help').attr('title', 'false');
console.log(`.attr('title', 'false'): title ${state(help, 'title')}`);
$('#help').attr('title', null);
console.log(`.attr('title', null): title ${state(help, 'title')}`);
$('#panel').removeAttr('data-mode');
console.log(`.removeAttr('data-mode'): data-mode ${state(panel, 'data-mode')}`); // Prints what prefs/attr-false.js logged in jsdom (recorded.mjs).
import { show } from './show.mjs';
show('prefs/attr-false.html'); Output
.attr('title', false): title removed
.attr('data-mode', false): data-mode removed
.attr('aria-hidden', false): aria-hidden kept as "false"
.attr('title', 'false'): title kept as "false"
.attr('title', null): title removed
.removeAttr('data-mode'): data-mode removed
(jQuery 3.7.1 logged other lines:)
line 1: .attr('title', false): title kept as "false"
line 2: .attr('data-mode', false): data-mode kept as "false"
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node attr-false.mjs
The title and the data-mode attribute went on 4.0.0 and stayed as "false" on 3.7.1. Code that stored the text on
purpose, often a data- flag that other code reads back, breaks one way; code that meant to remove the attribute and
got lucky with a boolean one works by accident. Say what you mean on both versions: .attr(name, 'false') for the
text, as jQuery Migrate advises (warnings.md), and
.removeAttr(name) for removal. .attr(name, null) removed the title on both versions as well.
Checkpoint
Check: .attr(name, false) in jQuery 4
toggleClass() without a class name changes nothing in jQuery 4
toggleClass() has two families of forms. With a class name it flips that class, or sets it from a boolean state:
toggleClass('open', true) adds, toggleClass('open', false) removes. Without a class name, toggleClass(false) and
toggleClass() were an old trick that removed every class and later restored them; the form was deprecated in 3.0
and removed in 4.0 (.toggleClass()). The upgrade guide names the real danger: a
variable that “turned out to be undefined or false” removed all classes
(4.0 upgrade guide). The script runs each form from class="card open":
// toggle.js: every form of toggleClass() that legacy code uses, each one started from class="card open".
const $panel = $('#panel');
const tryIt = (label, call) => {
$panel.attr('class', 'card open');
call();
console.log(`${label} -> class ${JSON.stringify($panel.attr('class'))}`);
};
let flag;
tryIt('toggleClass(false)', () => $panel.toggleClass(false));
tryIt('toggleClass(flag), flag undefined', () => $panel.toggleClass(flag));
tryIt("toggleClass('open')", () => $panel.toggleClass('open'));
tryIt("toggleClass('open', false)", () => $panel.toggleClass('open', false));
tryIt("toggleClass('open', true)", () => $panel.toggleClass('open', true));
tryIt("toggleClass('open', flag), flag undefined", () => $panel.toggleClass('open', flag));
tryIt("toggleClass('open', Boolean(flag))", () => $panel.toggleClass('open', Boolean(flag))); // Prints what prefs/toggle.js logged in jsdom (recorded.mjs).
import { show } from './show.mjs';
show('prefs/toggle.html'); Output
toggleClass(false) -> class "card open"
toggleClass(flag), flag undefined -> class "card open"
toggleClass('open') -> class "card"
toggleClass('open', false) -> class "card"
toggleClass('open', true) -> class "card open"
toggleClass('open', flag), flag undefined -> class "card"
toggleClass('open', Boolean(flag)) -> class "card"
(jQuery 3.7.1 logged other lines:)
line 1: toggleClass(false) -> class ""
line 2: toggleClass(flag), flag undefined -> class ""
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node toggle.mjs
On 3.7.1 the two forms without a class name wiped the panel’s classes; on 4.0.0 they changed nothing. The other
lines were the same on both, and one of them is a bug of its own: toggleClass('open', flag) with an undefined flag
toggled, because the state must be “a boolean (not just truthy/falsy)” (.toggleClass()).
Boolean(flag) turns it into one and removed open as intended.
Key idea
Give toggleClass() a class name and a real boolean, or use addClass() and removeClass(): the forms without a name are gone, and an undefined state flips the class.
Checkpoint
Check: The forms of toggleClass()
jQuery Migrate finds the changes; native code says what it means
The four changes leave no error to search for, but jQuery Migrate’s development build reports each one, and you can start before the upgrade: Migrate 3.6.0 on jQuery 3.7.1 already warns about them while the calls still behave the old way. The upgrade script below runs the four calls on both pairs:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Warnings on jQuery 3. Migrate 3.6.0 on jQuery 3.7.1 already warned about all four calls of the settings page, while they still behaved the jQuery 3 way.
- Fix them first. Read state with .prop(), store "false" as a string, remove attributes with removeAttr(), and keep classes in your own data. The page then behaves the same on 3 and on 4.
- Upgrade with Migrate 4. On jQuery 4.0.0, Migrate 4.0.2 restores the jQuery 3 behaviour of each call it finds and warns, so a call you missed still works while it tells you where it is.
- Remove Migrate. When the pages log no warning, remove Migrate: it is an upgrade tool, and jQuery 4 behaves as the fixed code expects.
// upgrade.js: the four changed behaviours of this lesson, run with jQuery Migrate on the page.
const q = (value) => JSON.stringify(value);
const box = (id) => document.getElementById(id);
console.log(`.attr('checked') of news: ${q($('#news').attr('checked'))}`);
$('#calls').attr('checked', 'yes');
console.log(`.attr('checked', 'yes') stores ${q(box('calls').getAttribute('checked'))}`);
$('#help').attr('title', false);
console.log(`.attr('title', false): the title is ${box('help').hasAttribute('title') ? 'kept' : 'removed'}`);
let flag;
$('#panel').toggleClass(flag);
console.log(`toggleClass(flag), flag undefined -> class ${q($('#panel').attr('class'))}`); // Prints what prefs/upgrade.js logged in jsdom: first on jQuery 3.7.1 with jQuery Migrate 3.6.0, then on jQuery 4.0.0
// with jQuery Migrate 4.0.2 (recorded.mjs).
import { show } from './show.mjs';
console.log('jQuery 3.7.1 with jQuery Migrate 3.6.0:');
show('prefs/migrate3.html');
console.log('jQuery 4.0.0 with jQuery Migrate 4.0.2:');
show('prefs/migrate4.html'); Output
jQuery 3.7.1 with jQuery Migrate 3.6.0:
JQMIGRATE: Migrate is installed with logging active, version 3.6.0
(warning) JQMIGRATE: Boolean attribute 'checked' value is different from its lowercased name
.attr('checked') of news: "checked"
(warning) JQMIGRATE: Boolean attribute 'checked' is not set to its lowercased name
.attr('checked', 'yes') stores "checked"
(warning) JQMIGRATE: Setting the non-ARIA non-boolean attribute 'title' to false
.attr('title', false): the title is kept
(warning) JQMIGRATE: jQuery.fn.toggleClass( boolean ) is deprecated
toggleClass(flag), flag undefined -> class ""
jQuery 4.0.0 with jQuery Migrate 4.0.2:
JQMIGRATE: Migrate is installed with logging active, version 4.0.2
(warning) JQMIGRATE: Boolean attribute 'checked' value is different from its lowercased name
.attr('checked') of news: "checked"
(warning) JQMIGRATE: Boolean attribute 'checked' is not set to its lowercased name
.attr('checked', 'yes') stores "checked"
(warning) JQMIGRATE: Setting the non-ARIA non-boolean attribute 'title' to false
.attr('title', false): the title is kept
(warning) JQMIGRATE: jQuery.fn.toggleClass( [ boolean ] ) is removed
toggleClass(flag), flag undefined -> class ""
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node upgrade.mjs
On jQuery 4.0.0, Migrate 4.0.2 also brought the jQuery 3 behaviour back for each call, so a missed line keeps working while its warning names it: boolean-attributes, attr-false and toggleClass-bool in Migrate’s list (warnings.md). That makes it an upgrade tool, not a place to stay: once no page logs a warning, remove it.
Code that leaves jQuery behind can say what it means directly, with properties, classList and toggleAttribute()
(DOM Standard):
// native.js: the same work without jQuery, each line saying what it means.
const panel = document.getElementById('panel');
const news = document.getElementById('news');
const help = document.getElementById('help');
const q = (value) => JSON.stringify(value);
console.log(`news.checked: ${news.checked}; news.hasAttribute('checked'): ${news.hasAttribute('checked')}`);
news.checked = false;
console.log(`after news.checked = false: ${news.checked}, the attribute still there: ${news.hasAttribute('checked')}`);
let flag;
panel.classList.toggle('open', Boolean(flag));
console.log(`classList.toggle('open', Boolean(flag)): ${q(panel.className)}`);
panel.classList.add('open');
console.log(`classList.add('open'): ${q(panel.className)}, contains: ${panel.classList.contains('open')}`);
panel.toggleAttribute('hidden', true);
console.log(`toggleAttribute('hidden', true): ${q(panel.getAttribute('hidden'))}`);
panel.toggleAttribute('hidden', false);
console.log(`toggleAttribute('hidden', false): hidden ${panel.hasAttribute('hidden') ? 'kept' : 'removed'}`);
help.removeAttribute('title');
panel.setAttribute('aria-hidden', 'false');
console.log(`removeAttribute('title'): ${help.hasAttribute('title') ? 'kept' : 'removed'}; aria-hidden: ${q(panel.getAttribute('aria-hidden'))}`); // Prints what prefs/native.js logged in jsdom (recorded.mjs).
import { show } from './show.mjs';
show('prefs/native.html'); Output
news.checked: true; news.hasAttribute('checked'): true
after news.checked = false: false, the attribute still there: true
classList.toggle('open', Boolean(flag)): "card"
classList.add('open'): "card open", contains: true
toggleAttribute('hidden', true): ""
toggleAttribute('hidden', false): hidden removed
removeAttribute('title'): removed; aria-hidden: "false"
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node native.mjs
Each jQuery call of this lesson has a native form that says the same thing:
| jQuery | Native |
|---|---|
.prop('checked'), .prop('checked', true) |
el.checked, el.checked = true |
.addClass(c), .removeClass(c), .hasClass(c) |
el.classList.add(c), remove(c), contains(c) |
.toggleClass(c, Boolean(on)) |
el.classList.toggle(c, Boolean(on)) |
.attr('hidden', flag) on jQuery 3 |
el.toggleAttribute('hidden', flag) |
.removeAttr(name) |
el.removeAttribute(name) |
.attr('aria-hidden', 'false') |
el.setAttribute('aria-hidden', 'false') |
Checkpoint
Check: Migrate warnings and native code
Practise: helpers that mean the same on jQuery 3 and 4
Five small helpers of an old settings page rely on the behaviour that changed. Rewrite them so each does what its comment says on both versions; the sample tests run every helper on a stand-in of each and run in your browser:
Exercise · Medium · JavaScript
Fix attribute code so jQuery 3 and 4 agree
The settings page of a shop has five small helpers in settings.mjs, written years ago for jQuery 1.x and 3.x. Each one relies on behaviour that jQuery 4 changed, and some were never right. Rewrite them so that every helper does what its comment says on jQuery 3 and on jQuery 4, which the tests check by running each helper on both.
isTicked($box)returnstruewhen the checkbox is ticked now, as the user sees it.tick($box)ticks the checkbox, even after the user has unticked it.clearTitle($el)removes the element'stitleattribute.saveClasses($el)keeps the element's classes in the element's jQuery data under the keysavedClassand removes theclassattribute;restoreClasses($el)puts the saved classes back.
The sample tests run without a browser, so they pass two small stand-ins for jQuery, one that behaves like jQuery 3.7 and one like jQuery 4.0 in exactly the calls this lesson covers: .attr(), .removeAttr(), .prop(), .toggleClass(), .addClass(), .removeClass() and .data(). A checkbox in the stand-ins keeps its attribute and its live state apart, as a browser does.
Starter code · settings.mjs
/** True when the checkbox is ticked now, as the user sees it. @param {object} $box */
export function isTicked($box) {
return Boolean($box.attr('checked'));
}
/** Ticks the checkbox, even after the user has unticked it. @param {object} $box */
export function tick($box) {
$box.attr('checked', true);
}
/** Removes the element's title attribute. @param {object} $el */
export function clearTitle($el) {
$el.attr('title', false);
}
/** Keeps the element's classes in its jQuery data (savedClass) and removes the class attribute. @param {object} $el */
export function saveClasses($el) {
$el.toggleClass(false);
}
/** Puts the classes that saveClasses() kept back on the element. @param {object} $el */
export function restoreClasses($el) {
$el.toggleClass();
} The sample tests · settings.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { isTicked, tick, clearTitle, saveClasses, restoreClasses } from './settings.mjs';
// Two stand-ins for jQuery, for one element at a time, in the calls this lesson covers. An element keeps its
// attributes apart from its live checked state, as a browser does: a click or .prop() sets the state, and from then on
// the attribute no longer decides it. The v3 stand-in behaves as jQuery 3.7 does, the v4 one as jQuery 4.0.
const BOOLEAN = new Set(['checked', 'selected', 'disabled', 'readonly', 'required', 'multiple', 'hidden', 'open']);
function element(attrs) {
return {
attrs: new Map(Object.entries(attrs)),
dirty: false,
state: false,
get checked() {
return this.dirty ? this.state : this.attrs.has('checked');
},
set checked(value) {
this.dirty = true;
this.state = Boolean(value);
},
};
}
const click = (el) => {
el.checked = !el.checked;
};
function standIn(version) {
return (el) => {
const data = (el.data ??= new Map());
const classes = () => (el.attrs.get('class') ?? '').split(/\s+/).filter(Boolean);
const $el = {
attr(name, value) {
if (value === undefined) {
if (!el.attrs.has(name)) return undefined;
return version === 3 && BOOLEAN.has(name) ? name : el.attrs.get(name);
}
const removes = value === null || (value === false && (version === 4 ? !name.startsWith('aria-') : BOOLEAN.has(name)));
if (removes) el.attrs.delete(name);
else el.attrs.set(name, version === 3 && BOOLEAN.has(name) ? name : String(value));
return $el;
},
removeAttr(name) {
el.attrs.delete(name);
return $el;
},
prop(name, value) {
if (name !== 'checked') throw new Error('the stand-in knows only the checked property');
if (value === undefined) return el.checked;
el.checked = value;
return $el;
},
addClass(name) {
el.attrs.set('class', [...new Set([...classes(), name])].join(' '));
return $el;
},
removeClass(name) {
el.attrs.set('class', classes().filter((c) => c !== name).join(' '));
return $el;
},
toggleClass(name, state) {
if (typeof name === 'string') {
const on = state === undefined ? !classes().includes(name) : state;
return on ? $el.addClass(name) : $el.removeClass(name);
}
// toggleClass(), toggleClass(false) and toggleClass(undefined): jQuery 3 stashed and restored every class;
// jQuery 4 removed these forms, so they change nothing.
if (version === 3) {
if (classes().length) {
data.set('__className__', el.attrs.get('class'));
el.attrs.set('class', '');
} else if (name !== false) {
el.attrs.set('class', data.get('__className__') ?? '');
}
}
return $el;
},
data(key, value) {
if (value === undefined) return data.get(key);
data.set(key, value);
return $el;
},
};
return $el;
};
}
const VERSIONS = [3, 4];
test('isTicked() reports what the user sees, on jQuery 3 and 4', () => {
for (const v of VERSIONS) {
const $ = standIn(v);
const news = element({ type: 'checkbox', checked: '' });
assert.equal(isTicked($(news)), true, `jQuery ${v}: a box ticked in the HTML`);
click(news);
assert.equal(isTicked($(news)), false, `jQuery ${v}: the same box after the user unticked it`);
const calls = element({ type: 'checkbox' });
click(calls);
assert.equal(isTicked($(calls)), true, `jQuery ${v}: a box the user ticked`);
}
});
test('tick() ticks a box, even one the user unticked, on jQuery 3 and 4', () => {
for (const v of VERSIONS) {
const $ = standIn(v);
const sms = element({ type: 'checkbox', checked: 'checked' });
click(sms);
tick($(sms));
assert.equal(sms.checked, true, `jQuery ${v}: the box the user had unticked`);
const calls = element({ type: 'checkbox' });
tick($(calls));
assert.equal(calls.checked, true, `jQuery ${v}: a box that was never ticked`);
}
});
test('clearTitle() removes the title attribute on jQuery 3 and 4', () => {
for (const v of VERSIONS) {
const $ = standIn(v);
const help = element({ href: '/help/', title: 'Help with settings' });
clearTitle($(help));
assert.equal(help.attrs.has('title'), false, `jQuery ${v}: the title attribute`);
assert.equal(help.attrs.get('href'), '/help/', `jQuery ${v}: the other attributes stay`);
}
});
test('saveClasses() and restoreClasses() keep and bring back the classes on jQuery 3 and 4', () => {
for (const v of VERSIONS) {
const $ = standIn(v);
const panel = element({ class: 'card open' });
saveClasses($(panel));
assert.equal((panel.attrs.get('class') ?? '').trim(), '', `jQuery ${v}: no class while saved`);
assert.equal($(panel).data('savedClass'), 'card open', `jQuery ${v}: the saved classes`);
restoreClasses($(panel));
assert.equal(panel.attrs.get('class'), 'card open', `jQuery ${v}: the classes restored`);
}
}); A hint
Ask what each helper means, then pick the call that says exactly that on both versions: the live state of a box is a property (.prop('checked'), .prop('checked', true)), removing an attribute is .removeAttr(), and saving classes is .data('savedClass', $el.attr('class')) followed by .removeAttr('class'), with .attr('class', …) to restore them. None of these calls changed in jQuery 4.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then the questions of the lesson, one at a time; the ones on the ideas you have answered best come last, and every choice says what it gets right or misses:
Practice that adapts to you
Practise this lesson
12 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): when do you use .prop() rather than .attr()?
Use .prop() for the live state of an element: checked, selected, disabled and the current value, which
change as the user clicks and types. Use .attr() for what the HTML says, such as href, title, data- and
aria- attributes, or a field’s default. For a checkbox the two differ as soon as the user clicks it: the checked
attribute keeps the default from the HTML, which form.reset() brings back, while the checked property follows the
click. .attr('checked') was never a test of the state, and on jQuery 4 it even returns "" for <input checked>,
which is falsy.
The bank of this module has two deeper questions about upgrades: a “remember me” box that stopped working, and an audit of the silent attribute changes before jQuery 4 goes live. Draft an answer to each before you open its first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to senior
A login form's "Remember me" box is ticked in the HTML. Since the upgrade to jQuery 4, nobody is remembered; the code decides with if ($('#remember').attr('checked')). Explain what changed, why the code was already wrong on jQuery 3, and fix it.
-
Mid to senior
Before a large site moves from jQuery 3.7 to 4.0, you are asked to find every place where .attr() and toggleClass() will change behaviour without an error. How do you plan the audit, and how do you know you are done?
Key takeaways
- Attributes are the HTML’s defaults and properties the live state: read and set checked, selected, disabled and the
current value with
.prop()or.val(), and remember that a checkbox’s.val()is its value, not its state. - jQuery 4 returns boolean attributes as written,
""for<input checked>, and stores what it is given, soif ($box.attr('checked'))and.attr('checked', true)now mean something else. .attr(name, false)removes every attribute butaria-*ones in jQuery 4; write.attr(name, 'false')for the text and.removeAttr(name)for removal.toggleClass()without a class name changes nothing in jQuery 4, and an undefined state flips the class, so pass a class name andBoolean(state).- jQuery Migrate 3.6.0 warns about these changes on jQuery 3.7.1 and Migrate 4.0.2 on 4.0.0, and native code says the
same with properties,
classListandtoggleAttribute().
Terms in this lesson
- boolean attribute
- An HTML attribute such as checked, selected or disabled whose presence means true and absence false. Its only valid values are an empty string and its own name; true and false are not allowed.
- DOM property
- A value on the JavaScript object of an element, such as el.checked or el.value. The browser sets it from the HTML attribute when the page loads; after that, clicks and typing change the property and leave the attribute as the default.
References
- .attr() (OpenJS Foundation (jQuery API documentation))
- .prop() (OpenJS Foundation (jQuery API documentation))
- .toggleClass() (OpenJS Foundation (jQuery API documentation))
- jQuery Core 4.0 Upgrade Guide: toggleClass( Boolean | undefined ) removed (OpenJS Foundation (jQuery))
- jQuery Migrate: warnings and their meaning (OpenJS Foundation (jQuery))
- HTML Standard: boolean attributes (WHATWG)
- DOM Standard: toggleAttribute() (WHATWG)
- DOM Standard: DOMTokenList (classList) (WHATWG)
- jsdom (jsdom project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress