Skip to the content
On this page
  1. How the files are organised
  2. Experiment 1 — HTML formatting options
  3. Experiment 2 — Lists and an image
  4. Experiment 3 — Ten images aligned with a table
  5. Experiment 4 — A form with every control type
  6. Experiment 5 — Embed a calendar
  7. Experiment 6 — Mailing-list subscription form
  8. Experiment 7 — Style a registration form with CSS
  9. Experiment 8 — Responsive page with Flexbox and Grid
  10. Experiment 9 — Hover effects and transitions
  11. Experiment 10 — JavaScript string operations
  12. Experiment 11 — Form validation
  13. Experiment 12 — Time-based greeting
  14. Experiment 13 — Array and object manipulation
  15. Experiment 14 — Render JSON as a table
  16. Experiment 15 — Fetch weather data from an open API
  17. Experiment 16 — jQuery DOM manipulation
  18. Lab examination

16 experiments, each set out as 1. Question, 2. Aim, 3. Steps, 4. Programme, 5. Execution and Results.

Code lives in labs/course-7-web/.

NOTE

These run, twice over. Every page is opened in Chromium, served over http from a local server as a page should be, and what it shows is under 5. Execution and Results: the page's headings, or what a driver typed, clicked and read back, with screenshots. The interactive pages each have a driver beside them, _drive_10_string_ops.py and so on, run by tools/data-science/capture_lab_outputs.py. Separately, every JavaScript function is executed by tools/data-science/run_web_labs.js under Node 22 with jsdom, and its result asserted, and every HTML file is structurally checked — balanced tags, every <label for> resolving to a real id, every <img> carrying alt.

npm --prefix tools/data-science install       # jsdom and jQuery, once
node tools/data-science/run_web_labs.js       # from the repository root

Two things cannot be reached from where these pages are checked. jQuery's CDN, code.jquery.com, is served instead from npm's copy of the same release, 3.7.1, and the page's integrity attribute makes the browser check that it is the same file, byte for byte. The weather API needs a key and a network, so Experiment 15 shows the page's own offline path. The clock is fixed at 09:00 on 4 October 2026, Indian time, so the calendar and the greeting show the same thing on every run.

How the files are organised

Each experiment that has logic is split in two:

labs/course-7-web/
    10_string_ops.js        ← pure functions, exported — testable
    10_string_ops.html      ← the browser page that uses them

That split is what makes the labs verifiable. A function that takes its input as a parameter and returns a value can be asserted; one that reads document.getElementById(…) and writes to the page cannot be, without a browser. The .js file holds the logic, the .html file holds the wiring.

The examiner will ask you to demonstrate in a browser, so know both halves.


Experiment 1 — HTML formatting options

1. Question

Write a page that uses the HTML formatting options: bold, italics, underline, headings H1–H6, font type, size and colour, a coloured or image background, paragraphs, line breaks, a horizontal rule and <pre>.

2. Aim

Use HTML's formatting elements, and know which are obsolete and which carry meaning.

3. Steps

  1. Set up the head: charset, viewport, title and styles.
  2. Show the six heading levels.
  3. Bold, italics and underline.
  4. Paragraphs, line breaks and a horizontal rule.
  5. Set the font type, size and colour.
  6. Give a background colour and a background image.
  7. Keep the spacing with pre.
  8. Show the other inline formatting.

THE EXAM POINT

The syllabus asks for the <font> tag, which is obsolete — removed in HTML5. Show that you know it existed and that CSS replaced it:

<font face="Georgia" size="5" color="blue">Obsolete since HTML5</font>
<span style="font-family:Georgia; font-size:20px; color:blue">The modern way</span>

Likewise <b>, <i> and <u> still render, but <strong>, <em> and a CSS underline carry meaning. Saying so is worth a mark.

<pre> is the only element that preserves whitespace and newlines. Everything else collapses runs of spaces to one.

4. Programme

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head: charset, viewport, title and styles -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 1 — HTML formatting options</title>
<style>
  body { font-family: Georgia, "Times New Roman", serif; line-height: 1.6;
         max-width: 760px; margin: 2rem auto; padding: 0 1rem;
         background-color: #f4f6f9; }
  pre  { background: #1e2430; color: #e6e9ef; padding: 1rem; border-radius: 6px;
         overflow-x: auto; }
  .bg-image { background-image: url("images/pattern.svg");
              background-repeat: repeat; padding: 1rem; border-radius: 6px; }
</style>
</head>
<body>

<!-- Step 2: Show the six heading levels -->
<h1>Heading level 1 — H1</h1>
<h2>Heading level 2 — H2</h2>
<h3>Heading level 3 — H3</h3>
<h4>Heading level 4 — H4</h4>
<h5>Heading level 5 — H5</h5>
<h6>Heading level 6 — H6</h6>

<!-- Step 3: Bold, italics and underline -->
<h2>(a) (b) (c) Bold, italics and underline</h2>
<p>
  <strong>Bold with &lt;strong&gt;</strong> and <b>bold with &lt;b&gt;</b>.<br>
  <em>Italic with &lt;em&gt;</em> and <i>italic with &lt;i&gt;</i>.<br>
  <u>Underlined with &lt;u&gt;</u> and
  <span style="text-decoration: underline;">underlined with CSS</span>.
</p>
<p>
  <strong>&lt;strong&gt;</strong> and <strong>&lt;em&gt;</strong> are
  <em>semantic</em> — they mean "important" and "emphasised", and a screen
  reader changes its tone for them. <b>&lt;b&gt;</b> and <i>&lt;i&gt;</i> are
  purely presentational. Prefer the semantic ones.
</p>

<!-- Step 4: Paragraphs, line breaks and a horizontal rule -->
<h2>(g) Paragraphs, (h) line breaks, (i) horizontal rule</h2>
<p>This is one paragraph. A paragraph is a block element, so the browser puts
   space above and below it automatically.</p>
<p>Line one<br>Line two, after a &lt;br&gt;<br>Line three</p>
<p>Use <code>&lt;br&gt;</code> only where the line break is part of the content
   — an address, a poem. Never <code>&lt;br&gt;&lt;br&gt;</code> for spacing;
   that is what CSS margins are for.</p>
<hr>

<!-- Step 5: Set the font type, size and colour -->
<h2>(e) Font type, size and colour</h2>
<p style="font-family: Georgia, serif; font-size: 20px; color: #2b4c7e;">
  Georgia, 20px, navy — set with CSS.
</p>
<p style="font-family: 'Courier New', monospace; font-size: 14px; color: #c92a2a;">
  Courier New, 14px, red — set with CSS.
</p>
<p>
  <font face="Georgia" size="5" color="blue">The &lt;font&gt; tag still
  renders, but it was removed in HTML5 and must not be used.</font>
</p>

<!-- Step 6: Give a background colour and a background image -->
<h2>(f) Background — colour and image</h2>
<p>The page background is <code>#f4f6f9</code>, set on <code>body</code>.</p>
<div class="bg-image">
  <p>This block uses <code>background-image</code> with
     <code>background-repeat: repeat</code>.</p>
</div>

<!-- Step 7: Keep the spacing with pre -->
<h2>(j) The pre tag</h2>
<pre>
    &lt;pre&gt; preserves whitespace
        and     line     breaks
    exactly as written.

    Every other element collapses
    runs   of   spaces   into   one.
</pre>

<hr>
<!-- Step 8: Show the other inline formatting -->
<h2>Other inline formatting worth knowing</h2>
<p>
  <mark>Highlighted</mark> ·
  <small>Small print</small> ·
  <del>Deleted</del> <ins>Inserted</ins> ·
  H<sub>2</sub>O · x<sup>2</sup> ·
  <code>inline code</code> ·
  <abbr title="Document Object Model">DOM</abbr> ·
  <q>a short quotation</q>
</p>
<blockquote cite="https://www.w3.org/">
  <p>The power of the Web is in its universality. Access by everyone
     regardless of disability is an essential aspect.</p>
  <footer>— Tim Berners-Lee</footer>
</blockquote>

</body>
</html>

5. Execution and Results

OUTPUT

opened 01_formatting.html in Chromium, 900 px wide: "Experiment 1 — HTML formatting options"
  H1  Heading level 1 — H1
  H2  Heading level 2 — H2
  H3  Heading level 3 — H3
  H4  Heading level 4 — H4
  H5  Heading level 5 — H5
  H6  Heading level 6 — H6
  H2  (a) (b) (c) Bold, italics and underline
  H2  (g) Paragraphs, (h) line breaks, (i) horizontal rule
  H2  (e) Font type, size and colour
  H2  (f) Background — colour and image
  H2  (j) The pre tag
  H2  Other inline formatting worth knowing
  elements: 12 p
  [screenshot 1: the whole page]

01_formatting.html: screenshot 1 of 1, taken while the program ran

RESULT

Every option renders: the six heading levels, the semantic and presentational bold and italic, the CSS fonts, the patterned background, and the <pre> block with its spacing kept.

Experiment 2 — Lists and an image

1. Question

Write a page with ordered, unordered and nested lists, and an image.

2. Aim

Use HTML's three list types, nest them correctly, and add an image with alt text.

3. Steps

  1. Set up the head: charset, viewport, title and styles.
  2. An ordered list, and its type and start.
  3. An unordered list.
  4. A nested list, inside an li.
  5. A description list.
  6. An image, with alt text and a caption.

THE MISTAKE EVERYONE MAKES

A nested list goes inside an <li>, not between two of them.

<ul>
  <li>Parent
    <ul><li>Child</li></ul>          <!-- correct: inside the li -->
  </li>
</ul>

<ul>
  <li>Parent</li>
  <ul><li>Child</li></ul>            <!-- WRONG: a ul directly inside a ul -->
</ul>

Both render similarly in a forgiving browser, but only the first validates, and only the first is announced correctly by a screen reader.

There are three list types: <ol> ordered, <ul> unordered, <dl> description. type on <ol> gives 1, A, a, I, i.

4. Programme

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head: charset, viewport, title and styles -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 2 — Lists and an image</title>
<style>
  body { font-family: system-ui, sans-serif; line-height: 1.6;
         max-width: 760px; margin: 2rem auto; padding: 0 1rem; }
  dt { font-weight: 700; margin-top: .6rem; }
  dd { margin-left: 1.5rem; color: #444; }
  figure { margin: 1.5rem 0; }
  figcaption { font-size: .9rem; color: #666; }
  img { max-width: 100%; height: auto; border-radius: 6px; }
</style>
</head>
<body>

<h1>Lists and an image</h1>

<!-- Step 2: An ordered list, and its type and start -->
<h2>(a) Ordered list</h2>
<ol>
  <li>Semester I</li>
  <li>Semester II</li>
  <li>Semester III</li>
</ol>

<p>The <code>type</code> attribute changes the marker, and
   <code>start</code> changes where the numbering begins:</p>
<ol type="I" start="3">
  <li>Third, numbered with Roman numerals</li>
  <li>Fourth</li>
</ol>

<!-- Step 3: An unordered list -->
<h2>(b) Unordered list</h2>
<ul>
  <li>NumPy</li>
  <li>Pandas</li>
  <li>Matplotlib</li>
</ul>

<!-- Step 4: A nested list, inside an li -->
<h2>(c) Nested list</h2>
<ul>
  <li>Year I
    <ul>
      <li>Computer Fundamentals</li>
      <li>Problem Solving in C</li>
    </ul>
  </li>
  <li>Year II
    <ul>
      <li>Database Management Systems</li>
      <li>Data Science with R</li>
      <li>Web Technologies
        <ol type="a">
          <li>HTML</li>
          <li>CSS</li>
          <li>JavaScript</li>
        </ol>
      </li>
    </ul>
  </li>
</ul>

<p><strong>Note the nesting rule:</strong> a nested list goes
   <em>inside</em> an <code>&lt;li&gt;</code>, never between two of them.
   Both forms render similarly, but only this one validates and only this one
   is announced correctly by a screen reader.</p>

<!-- Step 5: A description list -->
<h2>Description list</h2>
<dl>
  <dt>DOM</dt>
  <dd>Document Object Model — the browser's in-memory tree of the page.</dd>
  <dt>JSON</dt>
  <dd>JavaScript Object Notation — a text format for exchanging data.</dd>
  <dt>CSS</dt>
  <dd>Cascading Style Sheets — the presentation layer.</dd>
</dl>

<!-- Step 6: An image, with alt text and a caption -->
<h2>(d) Image</h2>
<figure>
  <img src="images/campus.svg" alt="A stylised drawing of the college campus"
       width="400" height="250">
  <figcaption>The campus. The <code>alt</code> text describes the image for
    screen readers and displays if the file fails to load.</figcaption>
</figure>

</body>
</html>

5. Execution and Results

OUTPUT

opened 02_lists.html in Chromium, 900 px wide: "Experiment 2 — Lists and an image"
  H1  Lists and an image
  H2  (a) Ordered list
  H2  (b) Unordered list
  H2  (c) Nested list
  H2  Description list
  H2  (d) Image
  elements: 2 p, 18 li, 1 img
  [screenshot 1: the whole page]

02_lists.html: screenshot 1 of 1, taken while the program ran

RESULT

The ordered list continues from III in Roman numerals, the nested list reaches three levels, and the image loads with its caption.

Experiment 3 — Ten images aligned with a table

1. Question

Align ten images using a table.

2. Aim

Lay images out in a table as asked, and then the way HTML5 intends, with CSS Grid.

3. Steps

  1. Set up the head: charset, viewport, title and styles.
  2. Arrange ten images in a table.
  3. Say why a table is the wrong tool for layout.
  4. Lay out the same gallery with CSS Grid.

STATE THE CAVEAT

State the caveat, because the examiner is listening for it. This experiment asks you to use a table for layout, which HTML5 forbids: tables are for tabular data, and a screen reader announces "table, 2 rows, 5 columns" to someone who cannot see the images. Do it as asked — then add the modern equivalent, which is three lines and also responsive:

.gallery { display: grid; gap: 8px;
           grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }

object-fit: cover makes images of different aspect ratios fill an identical box by cropping, rather than stretching. display: block removes the few pixels of space under an inline image.

4. Programme

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head: charset, viewport, title and styles -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 3 — Ten images aligned with a table</title>
<style>
  body { font-family: system-ui, sans-serif; line-height: 1.6;
         max-width: 860px; margin: 2rem auto; padding: 0 1rem; }
  table { border-collapse: collapse; margin: 1rem 0; }
  caption { caption-side: top; text-align: left; font-weight: 700;
            padding-bottom: .5rem; }
  td { padding: 4px; border: 1px solid #d6dbe4; vertical-align: middle; }
  td img { width: 150px; height: 110px; object-fit: cover; display: block;
           border-radius: 4px; }
  .wide img { width: 304px; }
  .note { background: #fff8e6; border-left: 4px solid #f6c761;
          padding: .8rem 1rem; border-radius: 0 6px 6px 0; }
  .gallery { display: grid; gap: 8px;
             grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
  .gallery img { width: 100%; height: 110px; object-fit: cover;
                 display: block; border-radius: 4px; }
</style>
</head>
<body>

<h1>Ten images aligned with a table</h1>

<!-- Step 2: Arrange ten images in a table -->
<table>
  <caption>Gallery — a 2 &times; 5 arrangement using colspan</caption>
  <tbody>
    <tr>
      <td><img src="images/1.svg" alt="Placeholder image one"></td>
      <td class="wide" colspan="2"><img src="images/2.svg" alt="Placeholder image two"></td>
      <td><img src="images/3.svg" alt="Placeholder image three"></td>
    </tr>
    <tr>
      <td><img src="images/4.svg" alt="Placeholder image four"></td>
      <td><img src="images/5.svg" alt="Placeholder image five"></td>
      <td><img src="images/6.svg" alt="Placeholder image six"></td>
      <td><img src="images/7.svg" alt="Placeholder image seven"></td>
    </tr>
    <tr>
      <td rowspan="1"><img src="images/8.svg" alt="Placeholder image eight"></td>
      <td class="wide" colspan="2"><img src="images/9.svg" alt="Placeholder image nine"></td>
      <td><img src="images/10.svg" alt="Placeholder image ten"></td>
    </tr>
  </tbody>
</table>

<!-- Step 3: Say why a table is the wrong tool for layout -->
<div class="note">
  <p><strong>The caveat this experiment is really about.</strong> The syllabus
  asks you to align images using a table, which is <em>layout</em>, and HTML5
  forbids it. Tables are for tabular data. A screen reader announces "table,
  3 rows, 4 columns" to someone who cannot see the images, which helps nobody.</p>
  <p>Do it as asked in the exam — then show the modern equivalent below, which
  is three lines of CSS and is also responsive.</p>
</div>

<!-- Step 4: Lay out the same gallery with CSS Grid -->
<h2>The same gallery with CSS Grid</h2>
<div class="gallery">
  <img src="images/1.svg" alt="Placeholder image one">
  <img src="images/2.svg" alt="Placeholder image two">
  <img src="images/3.svg" alt="Placeholder image three">
  <img src="images/4.svg" alt="Placeholder image four">
  <img src="images/5.svg" alt="Placeholder image five">
  <img src="images/6.svg" alt="Placeholder image six">
  <img src="images/7.svg" alt="Placeholder image seven">
  <img src="images/8.svg" alt="Placeholder image eight">
  <img src="images/9.svg" alt="Placeholder image nine">
  <img src="images/10.svg" alt="Placeholder image ten">
</div>

<pre><code>.gallery { display: grid; gap: 8px;
           grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }</code></pre>

<p><code>auto-fit</code> fits as many columns as will hold 150px and stretches
them to fill the row — a complete responsive gallery with no media queries.
<code>object-fit: cover</code> makes images of different aspect ratios fill an
identical box by cropping rather than stretching.</p>

</body>
</html>

5. Execution and Results

OUTPUT

opened 03_image_table.html in Chromium, 900 px wide: "Experiment 3 — Ten images aligned with a table"
  H1  Ten images aligned with a table
  H2  The same gallery with CSS Grid
  elements: 3 p, 20 img, 1 table
  [screenshot 1: the whole page]

03_image_table.html: screenshot 1 of 1, taken while the program ran

RESULT

The ten images sit in the table's grid, and again in the CSS Grid gallery below it.

Experiment 4 — A form with every control type

1. Question

Create a form with text boxes, radio buttons, check boxes, and reset and submit buttons.

2. Aim

Build a form from every kind of control, each labelled and named.

3. Steps

  1. Set up the head: charset, viewport, title and styles.
  2. Open the form.
  3. Text boxes, each with a label.
  4. Radio buttons, sharing one name.
  5. Check boxes.
  6. A select list and a file picker.
  7. Reset and submit buttons.

RADIO VS CHECKBOX, AND THE RULE BEHIND IT

Radio buttons in one group must share the same name — that is what makes them mutually exclusive, and different names give you three independent buttons that can all be on at once. Checkboxes sharing a name submit as multiple values under that key.

Every input needs a name, or it is not submitted at all. Every input needs a <label for> matching its id, or clicking the text does not focus the field and a screen reader has nothing to announce.

4. Programme

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head: charset, viewport, title and styles -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 4 — A form with every control type</title>
<style>
  body { font-family: system-ui, sans-serif; line-height: 1.6;
         max-width: 620px; margin: 2rem auto; padding: 0 1rem; }
  fieldset { border: 1px solid #ccd3dd; border-radius: 8px;
             padding: 1rem; margin-bottom: 1rem; }
  legend { font-weight: 700; color: #2b4c7e; padding: 0 .4rem; }
  label { display: block; margin-bottom: .3rem; font-weight: 600; }
  fieldset fieldset label { display: inline; font-weight: 400; margin-right: 1rem; }
  input[type="text"], input[type="email"], input[type="password"],
  input[type="number"], input[type="date"], select, textarea {
    width: 100%; padding: 9px 11px; margin-bottom: 1rem; font: inherit;
    border: 1px solid #ccd3dd; border-radius: 6px; }
  input:focus, select:focus, textarea:focus {
    border-color: #2b4c7e; outline: 3px solid rgba(43,76,126,.25); }
  button { padding: 10px 18px; border: 0; border-radius: 6px; font: inherit;
           cursor: pointer; margin-right: .5rem; }
  button[type="submit"] { background: #2b4c7e; color: #fff; }
  button[type="reset"]  { background: #e8ecf2; color: #333; }
</style>
</head>
<body>

<h1>Form controls</h1>

<!-- Step 2: Open the form -->
<form action="/submit" method="post">

  <fieldset>
    <!-- Step 3: Text boxes, each with a label -->
    <legend>(a) Text boxes</legend>

    <label for="name">Name</label>
    <input type="text" id="name" name="name" placeholder="Asha Kumari"
           autocomplete="name">

    <label for="email">Email</label>
    <input type="email" id="email" name="email" autocomplete="email">

    <label for="pw">Password</label>
    <input type="password" id="pw" name="password" autocomplete="new-password">

    <label for="age">Age</label>
    <input type="number" id="age" name="age" min="16" max="100" step="1">

    <label for="dob">Date of birth</label>
    <input type="date" id="dob" name="dob">

    <label for="about">About yourself</label>
    <textarea id="about" name="about" rows="3" maxlength="200"></textarea>
  </fieldset>

  <fieldset>
    <!-- Step 4: Radio buttons, sharing one name -->
    <legend>(b) Radio buttons — one choice only</legend>
    <input type="radio" id="gf" name="gender" value="F"><label for="gf">Female</label>
    <input type="radio" id="gm" name="gender" value="M"><label for="gm">Male</label>
    <input type="radio" id="go" name="gender" value="O"><label for="go">Other</label>
    <p><small>All three share <code>name="gender"</code>. That shared name is
      what makes them mutually exclusive — give them different names and all
      three can be selected at once.</small></p>
  </fieldset>

  <fieldset>
    <!-- Step 5: Check boxes -->
    <legend>(c) Check boxes — any number</legend>
    <input type="checkbox" id="s1" name="subjects" value="ds"><label for="s1">Data Science</label>
    <input type="checkbox" id="s2" name="subjects" value="st"><label for="s2">Statistics</label>
    <input type="checkbox" id="s3" name="subjects" value="cs"><label for="s3">Computer Science</label>
  </fieldset>

  <fieldset>
    <!-- Step 6: A select list and a file picker -->
    <legend>Select and file</legend>

    <label for="course">Programme</label>
    <select id="course" name="course">
      <option value="">— select —</option>
      <optgroup label="Science">
        <option value="ds">Data Science</option>
        <option value="st" selected>Statistics</option>
      </optgroup>
      <optgroup label="Commerce">
        <option value="bcom">B.Com.</option>
      </optgroup>
    </select>

    <label for="photo">Photograph</label>
    <input type="file" id="photo" name="photo" accept="image/*">
  </fieldset>

  <fieldset>
    <!-- Step 7: Reset and submit buttons -->
    <legend>(d) Reset and submit</legend>
    <button type="submit">Submit</button>
    <button type="reset">Reset</button>
  </fieldset>

</form>

<p><small><strong>Every input needs a <code>name</code></strong> — without one
the field is not submitted at all. Every input needs a <code>&lt;label
for&gt;</code> matching its <code>id</code>, or clicking the text does not
focus the field and a screen reader has nothing to announce. A placeholder is
not a label: it disappears the moment the user types.</small></p>

</body>
</html>

5. Execution and Results

OUTPUT

opened 04_form_controls.html in Chromium, 900 px wide: "Experiment 4 — A form with every control type"
checked Female, then Male: Female is now False, Male True -- one name, so one choice
what the form would send, from FormData:
  name=Asha Rao
  email=asha@example.com
  password=Passw0rd!
  age=19
  dob=2007-03-14
  about=Second-year data science student.
  gender=M
  subjects=ds
  subjects=cs
  course=ds
  [screenshot 1: filled in]
after Reset: name is '', Male checked False

04_form_controls.html: screenshot 1 of 1, taken while the program ran

The driver fills the form but does not submit it: it posts to /submit, and no server is behind that address. FormData shows what would be sent — one gender, two subjects.

RESULT

Checking Male clears Female, as one shared name makes them; the two subjects submit under one key; and Reset empties the form.

Experiment 5 — Embed a calendar

1. Question

Embed a calendar object in a web page.

2. Aim

Show a calendar three ways: the native date picker, an embedded calendar, and a month grid generated in JavaScript.

3. Steps

In HTML, 05_calendar.html:

  1. Set up the head: charset, viewport, title and styles.
  2. The native date picker.
  3. An embedded external calendar.
  4. Controls for a generated month grid.
  5. Fill the month list, and start at today's month.
  6. Draw the month, and move between months.

In JavaScript, 05_calendar.js:

  1. Name the months and days.
  2. Count the days in a month.
  3. Find the first weekday, and lay the month out in weeks.
  4. Test for a leap year.
  5. Draw the month into a table.

THREE MODERN ANSWERS

The syllabus says "embed a calendar object", which in 2004 meant an ActiveX control. Three modern answers, in increasing order of effort: the native date picker (one line), an embedded external calendar (an <iframe>), and a month grid you generate yourself, which is the version worth learning because it is pure logic.

The trick worth remembering: new Date(y, m, 0) is day zero of month m, which is the last day of month m − 1 — so it gives you the number of days in a month, leap years included, with no table and no arithmetic.

4. Programme

In HTML, 05_calendar.html:

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head: charset, viewport, title and styles -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 5 — Embed a calendar</title>
<style>
  body { font-family: system-ui, sans-serif; line-height: 1.6;
         max-width: 620px; margin: 2rem auto; padding: 0 1rem; }
  .controls { display: flex; gap: .5rem; align-items: center; margin: 1rem 0; }
  button, select, input { font: inherit; padding: 6px 10px;
                          border: 1px solid #ccd3dd; border-radius: 6px; }
  button { background: #2b4c7e; color: #fff; border: 0; cursor: pointer; }
  table { border-collapse: collapse; width: 100%; }
  caption { font-weight: 700; font-size: 1.1rem; padding-bottom: .5rem;
            color: #2b4c7e; }
  th, td { border: 1px solid #e2e7ee; padding: 8px; text-align: center; }
  th { background: #eef2f7; }
  td.empty { background: #fafbfd; }
  td.today { background: #2b8a3e; color: #fff; font-weight: 700; }
</style>
</head>
<body>

<h1>Embedding a calendar</h1>

<!-- Step 2: The native date picker -->
<h2>1. The native date picker — one line</h2>
<label for="dob">Date of birth</label>
<input type="date" id="dob" name="dob" min="1990-01-01" max="2010-12-31">
<p><small>Every modern browser renders its own calendar widget for
  <code>&lt;input type="date"&gt;</code>, localised and keyboard accessible,
  for no effort at all.</small></p>

<!-- Step 3: An embedded external calendar -->
<h2>2. An embedded external calendar</h2>
<pre><code>&lt;iframe src="https://calendar.google.com/calendar/embed?src=…"
        width="600" height="400" style="border:0"
        title="Academic calendar" loading="lazy"&gt;&lt;/iframe&gt;</code></pre>
<p><small>Shown as markup rather than live, because it needs a real calendar
  URL and network access. Note the <code>title</code> — an iframe without one
  is announced as "frame" and nothing else.</small></p>

<!-- Step 4: Controls for a generated month grid -->
<h2>3. A month grid, generated in JavaScript</h2>
<div class="controls">
  <button id="prev" type="button" aria-label="Previous month">&larr;</button>
  <select id="month" aria-label="Month"></select>
  <input type="number" id="year" min="1900" max="2100" style="width: 6rem"
         aria-label="Year">
  <button id="next" type="button" aria-label="Next month">&rarr;</button>
</div>

<table id="cal"></table>

<script type="module">
  import { renderCalendar, MONTH_NAMES } from "./05_calendar.js";

  const table = document.getElementById("cal");
  const monthSel = document.getElementById("month");
  const yearIn = document.getElementById("year");

  // Step 5: Fill the month list, and start at today's month
  MONTH_NAMES.forEach((n, i) => {
    const o = document.createElement("option");
    o.value = String(i + 1);
    o.textContent = n;
    monthSel.append(o);
  });

  const now = new Date();
  let year = now.getFullYear();
  let month = now.getMonth() + 1;

  // Step 6: Draw the month, and move between months
  function draw() {
    monthSel.value = String(month);
    yearIn.value = String(year);
    renderCalendar(year, month, table);
  }

  function shift(by) {
    month += by;
    if (month < 1)  { month = 12; year--; }
    if (month > 12) { month = 1;  year++; }
    draw();
  }

  document.getElementById("prev").addEventListener("click", () => shift(-1));
  document.getElementById("next").addEventListener("click", () => shift(1));
  monthSel.addEventListener("change", () => { month = Number(monthSel.value); draw(); });
  yearIn.addEventListener("change", () => { year = Number(yearIn.value); draw(); });

  draw();
</script>

</body>
</html>

In JavaScript, 05_calendar.js:

/* Experiment 5 — Embed a calendar object in a web page.
 *
 * The syllabus phrase "calendar object" dates from an era of ActiveX
 * controls. Three modern answers, in the HTML: the native <input type="date">
 * picker, an <iframe> embed, and a month grid generated here.
 *
 * This module is pure: it takes a year and month and returns data, with no
 * reference to `document`. That is what makes it testable — see
 * tools/run_web_labs.js.
 */

// Step 1: Name the months and days
export const MONTH_NAMES = [
  "January", "February", "March", "April", "May", "June",
  "July", "August", "September", "October", "November", "December"
];

export const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

// Step 2: Count the days in a month
/** Number of days in a month. `month` is 1-12.
 *
 * new Date(y, m, 0) is "day zero" of month m, which the Date constructor
 * normalises to the LAST day of month m-1. Because `month` here is 1-based
 * and the constructor is 0-based, passing `month` straight through lands on
 * the last day of the month we asked about — leap years included, with no
 * table and no arithmetic of our own.
 */
export function daysInMonth(year, month) {
  return new Date(year, month, 0).getDate();
}

// Step 3: Find the first weekday, and lay the month out in weeks
/** Weekday index (0 = Sunday) of the first of the month. */
export function firstWeekday(year, month) {
  return new Date(year, month - 1, 1).getDay();
}

/** A month as an array of 7-element week rows.
 *  Cells before the 1st and after the last are null. */
export function monthMatrix(year, month) {
  if (!Number.isInteger(month) || month < 1 || month > 12)
    throw new RangeError("month must be an integer from 1 to 12");

  const cells = Array(firstWeekday(year, month)).fill(null);
  for (let d = 1; d <= daysInMonth(year, month); d++) cells.push(d);
  while (cells.length % 7 !== 0) cells.push(null);

  const weeks = [];
  for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
  return weeks;
}

// Step 4: Test for a leap year
export function isLeapYear(y) {
  return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
}

// Step 5: Draw the month into a table
/** Render a month into a <table>. `doc` is injectable so jsdom can pass its
 *  own document; in a browser it defaults to the real one. */
export function renderCalendar(year, month, table, doc = document) {
  table.textContent = "";

  const caption = doc.createElement("caption");
  caption.textContent = `${MONTH_NAMES[month - 1]} ${year}`;
  table.append(caption);

  const thead = doc.createElement("thead");
  const hrow = doc.createElement("tr");
  for (const d of DAY_NAMES) {
    const th = doc.createElement("th");
    th.scope = "col";
    th.textContent = d;
    hrow.append(th);
  }
  thead.append(hrow);
  table.append(thead);

  const today = new Date();
  const isThisMonth = today.getFullYear() === year && today.getMonth() === month - 1;

  const tbody = doc.createElement("tbody");
  for (const week of monthMatrix(year, month)) {
    const tr = doc.createElement("tr");
    for (const day of week) {
      const td = doc.createElement("td");
      if (day === null) {
        td.className = "empty";
      } else {
        td.textContent = String(day);
        if (isThisMonth && day === today.getDate()) td.className = "today";
      }
      tr.append(td);
    }
    tbody.append(tr);
  }
  table.append(tbody);
  return table;
}

5. Execution and Results

OUTPUT

opened 05_calendar.html in Chromium, 900 px wide: "Experiment 5 — Embed a calendar"

October 2026   (today's month)
  SunMonTueWedThuFriSat
                1  2  3
    4  5  6  7  8  9 10
   11 12 13 14 15 16 17
   18 19 20 21 22 23 24
   25 26 27 28 29 30 31
  today, marked: 4
  [screenshot 1: October 2026, the month it opens on]

August 2026   (after Previous twice)
  SunMonTueWedThuFriSat
                      1
    2  3  4  5  6  7  8
    9 10 11 12 13 14 15
   16 17 18 19 20 21 22
   23 24 25 26 27 28 29
   30 31
  1 August 2026 is in column 6, a Saturday

February 2024   (February 2024, chosen from the list)
  SunMonTueWedThuFriSat
                1  2  3
    4  5  6  7  8  9 10
   11 12 13 14 15 16 17
   18 19 20 21 22 23 24
   25 26 27 28 29
  29 days: 2024 is a leap year
  [screenshot 2: February 2024]

05_calendar.html: screenshot 1 of 2, taken while the program ran

05_calendar.html: screenshot 2 of 2, taken while the program ran

Verified: February 2024 has 29 days, February 2025 has 28, and 1 August 2026 falls on a Saturday. The first and third are shown above; all three are asserted by the runner.

Note that JavaScript months are 0-indexed in the Date constructor (January is 0) but 1-indexed in an ISO string. Mixing the two is the classic date bug.

RESULT

The page opens on October 2026 with the 4th marked as today; it moves between months, and February 2024 has 29 days.

Experiment 6 — Mailing-list subscription form

1. Question

Create a form to subscribe to a mailing list.

2. Aim

Build a subscription form that asks for consent, and check it before it is sent.

3. Steps

  1. Set up the head, and link the stylesheet.
  2. Ask for the email and name.
  3. Ask for the topics.
  4. Ask for the frequency, format and consent.
  5. Check the email and the consent on submit.

CONSENT

An explicit consent checkbox, unticked by default, is not decoration — pre-ticked consent is unlawful under most data-protection regimes and is the kind of detail that separates a good answer from a complete one.

4. Programme

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head, and link the stylesheet -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 6 — Mailing list subscription form</title>
<link rel="stylesheet" href="07_styled_form.css">
</head>
<body>

<h1>Subscribe to the Data Science newsletter</h1>

<form id="subscribe" action="/subscribe" method="post" novalidate>

  <fieldset>
    <!-- Step 2: Ask for the email and name -->
    <legend>Your details</legend>

    <label for="email">Email address <span aria-hidden="true">*</span></label>
    <input type="email" id="email" name="email" required
           autocomplete="email" aria-describedby="email-error"
           placeholder="asha@example.com">
    <span id="email-error" class="error" role="alert"></span>

    <label for="name">Name</label>
    <input type="text" id="name" name="name" autocomplete="name"
           placeholder="Asha Kumari">

    <label for="org">College or organisation</label>
    <input type="text" id="org" name="org" autocomplete="organization">
  </fieldset>

  <fieldset>
    <!-- Step 3: Ask for the topics -->
    <legend>What would you like to hear about?</legend>
    <input type="checkbox" id="i1" name="topics" value="ds" checked>
    <label for="i1">Data Science</label>
    <input type="checkbox" id="i2" name="topics" value="ml">
    <label for="i2">Machine Learning</label>
    <input type="checkbox" id="i3" name="topics" value="viz">
    <label for="i3">Visualization</label>
    <input type="checkbox" id="i4" name="topics" value="jobs">
    <label for="i4">Jobs and internships</label>
  </fieldset>

  <fieldset>
    <!-- Step 4: Ask for the frequency, format and consent -->
    <legend>Preferences</legend>

    <label for="freq">How often?</label>
    <select id="freq" name="freq">
      <option value="d">Daily digest</option>
      <option value="w" selected>Weekly summary</option>
      <option value="m">Monthly round-up</option>
    </select>

    <fieldset>
      <legend>Format</legend>
      <input type="radio" id="fh" name="format" value="html" checked>
      <label for="fh">HTML</label>
      <input type="radio" id="ft" name="format" value="text">
      <label for="ft">Plain text</label>
    </fieldset>
  </fieldset>

  <input type="checkbox" id="consent" name="consent" required
         aria-describedby="consent-error">
  <label for="consent">I consent to receiving email at this address
    <span aria-hidden="true">*</span></label>
  <span id="consent-error" class="error" role="alert"></span>

  <p><button type="submit">Subscribe</button>
     <button type="reset">Clear</button></p>

  <p id="status" role="status" aria-live="polite"></p>
</form>

<p><small>The consent checkbox is <strong>unticked by default</strong> and is
  <code>required</code>. Pre-ticked consent is unlawful under most
  data-protection regimes, and an examiner who knows that is looking for
  it.</small></p>

<script type="module">
  import { validate } from "./11_validation.js";

  const form = document.getElementById("subscribe");

  // Step 5: Check the email and the consent on submit
  form.addEventListener("submit", e => {
    e.preventDefault();                       // demo only: nothing to POST to
    const email = form.elements.email.value;
    const box = document.getElementById("email-error");
    const consentBox = document.getElementById("consent-error");

    const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email.trim());
    box.textContent = emailOk ? "" : "Enter a valid email address";
    form.elements.email.classList.toggle("is-invalid", !emailOk);

    const consentOk = form.elements.consent.checked;
    consentBox.textContent = consentOk ? "" : "You must consent before subscribing";

    document.getElementById("status").textContent =
      emailOk && consentOk ? `Subscribed ${email.trim()}.` : "";
  });
</script>

</body>
</html>

5. Execution and Results

OUTPUT

opened 06_subscribe.html in Chromium, 900 px wide: "Experiment 6 — Mailing list subscription form"
submitted empty:  email says 'Enter a valid email address'
                  consent says 'You must consent before subscribing'
  [screenshot 1: submitted empty]
filled in:        status says 'Subscribed asha@example.com.'
  [screenshot 2: filled in and submitted]

06_subscribe.html: screenshot 1 of 2, taken while the program ran

06_subscribe.html: screenshot 2 of 2, taken while the program ran

The page borrows its look from 07_styled_form.css, so its styling is Experiment 7's.

RESULT

Submitted empty, the form asks for a valid email and for consent; filled in, it reports "Subscribed asha@example.com."

Experiment 7 — Style a registration form with CSS

1. Question

Style a registration form with CSS: different selectors, colours, borders and spacing.

2. Aim

Style a form with CSS, using every kind of selector.

3. Steps

In HTML, 07_styled_form.html:

  1. Set up the head, and link the stylesheet.
  2. Personal details.
  3. Account.
  4. Programme.

In CSS, 07_styled_form.css:

  1. Define the colours once, as custom properties.
  2. Lay out the page, the form and its fieldsets.
  3. Style the labels and the text-like controls.
  4. Show state with pseudo-classes.
  5. Style the error messages and the buttons.
  6. Respect the user's settings.

SELECTORS, AND TWO DETAILS

Selector types demonstrated: element (form), class, id, attribute (input[type="text"]), pseudo-class (:focus, :invalid), descendant, and a custom-property :root block.

Two details the examiner looks for. font: inherit — form controls use the operating system font unless told otherwise, so an unstyled input looks alien beside your text. And :not(:placeholder-shown) — without it, :invalid fires on an empty required field the moment the page loads, and the form is red before the user has typed anything.

4. Programme

In HTML, 07_styled_form.html:

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head, and link the stylesheet -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 7 — A CSS-styled student registration form</title>
<link rel="stylesheet" href="07_styled_form.css">
</head>
<body>

<h1>Student registration</h1>

<form id="reg" action="/register" method="post" novalidate>

  <fieldset>
    <!-- Step 2: Personal details -->
    <legend>Personal details</legend>

    <label for="name">Full name <span aria-hidden="true">*</span></label>
    <input type="text" id="name" name="name" required minlength="3" maxlength="50"
           placeholder="Asha Kumari" autocomplete="name"
           aria-describedby="name-error">
    <span id="name-error" class="error" role="alert"></span>

    <label for="email">Email <span aria-hidden="true">*</span></label>
    <input type="email" id="email" name="email" required
           placeholder="asha@nri.ac.in" autocomplete="email"
           aria-describedby="email-error">
    <span id="email-error" class="error" role="alert"></span>

    <label for="mobile">Mobile <span aria-hidden="true">*</span></label>
    <input type="tel" id="mobile" name="mobile" required
           pattern="[6-9][0-9]{9}" placeholder="9876543210"
           title="10-digit Indian mobile number" aria-describedby="mobile-error">
    <span id="mobile-error" class="error" role="alert"></span>

    <label for="dob">Date of birth</label>
    <input type="date" id="dob" name="dob" max="2010-12-31">

    <fieldset>
      <legend>Gender</legend>
      <input type="radio" id="gf" name="gender" value="F"><label for="gf">Female</label>
      <input type="radio" id="gm" name="gender" value="M"><label for="gm">Male</label>
      <input type="radio" id="go" name="gender" value="O"><label for="go">Other</label>
    </fieldset>
  </fieldset>

  <fieldset>
    <!-- Step 3: Account -->
    <legend>Account</legend>

    <label for="password">Password <span aria-hidden="true">*</span></label>
    <input type="password" id="password" name="password" required
           placeholder="At least 8 characters" autocomplete="new-password"
           aria-describedby="password-error">
    <span id="password-error" class="error" role="alert"></span>

    <label for="confirm">Confirm password <span aria-hidden="true">*</span></label>
    <input type="password" id="confirm" name="confirm" required
           placeholder="Type it again" autocomplete="new-password"
           aria-describedby="confirm-error">
    <span id="confirm-error" class="error" role="alert"></span>
  </fieldset>

  <fieldset>
    <!-- Step 4: Programme -->
    <legend>Programme</legend>

    <label for="course">Course</label>
    <select id="course" name="course">
      <option value="">— select —</option>
      <option value="ds" selected>Data Science</option>
      <option value="st">Statistics</option>
    </select>

    <label for="about">About yourself</label>
    <textarea id="about" name="about" rows="4" maxlength="200"
              placeholder="A sentence or two"></textarea>
  </fieldset>

  <input type="checkbox" id="terms" name="terms" required
         aria-describedby="terms-error">
  <label for="terms">I accept the terms <span aria-hidden="true">*</span></label>
  <span id="terms-error" class="error" role="alert"></span>

  <p><button type="submit">Register</button>
     <button type="reset">Clear</button></p>

  <p id="status" role="status" aria-live="polite"></p>
</form>

</body>
</html>

In CSS, 07_styled_form.css:

/* Experiment 7 — Style a registration form.
 * Demonstrates: element, class, id, attribute, pseudo-class and descendant
 * selectors; custom properties; colours, borders and spacing.
 */

/* Step 1: Define the colours once, as custom properties */
:root {
  --brand:  #2b4c7e;
  --brand-dark: #1f3a63;
  --danger: #c92a2a;
  --ok:     #2b8a3e;
  --line:   #ccd3dd;
  --ink:    #1f2733;
  --muted:  #5b6675;
}

*, *::before, *::after { box-sizing: border-box; }

/* Step 2: Lay out the page, the form and its fieldsets */
body {
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: var(--ink);
  background: #eef2f7;
  margin: 0;
  padding: 2rem 1rem;
}

h1 { max-width: 560px; margin: 0 auto 1rem; color: var(--brand); }
body > p { max-width: 560px; margin: 1rem auto; color: var(--muted); }

form {
  max-width: 560px;
  margin: 0 auto;
  padding: 1.5rem;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 2px 10px rgba(31, 39, 51, .06);
}

fieldset {
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 1rem 1rem .5rem;
  margin: 0 0 1.25rem;
}

legend {
  font-weight: 700;
  color: var(--brand);
  padding: 0 .4rem;
}

/* Step 3: Style the labels and the text-like controls */
label {
  display: block;
  margin-bottom: .3rem;
  font-weight: 600;
}

/* radio and checkbox labels sit beside their control, not above it */
input[type="radio"] + label,
input[type="checkbox"] + label {
  display: inline;
  font-weight: 400;
  margin-right: 1.2rem;
}

/* attribute selectors — one rule for every text-like control */
input[type="text"],
input[type="email"],
input[type="tel"],
input[type="password"],
input[type="number"],
input[type="date"],
select,
textarea {
  width: 100%;
  padding: 10px 12px;
  margin-bottom: 1rem;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #fff;
  font: inherit;              /* form controls do NOT inherit fonts by default */
  color: inherit;
  transition: border-color .18s, box-shadow .18s;
}

textarea { resize: vertical; min-height: 5rem; }

/* Step 4: Show state with pseudo-classes */
/* pseudo-classes for state */
input:focus, select:focus, textarea:focus {
  border-color: var(--brand);
  outline: 3px solid rgba(43, 76, 126, .25);
  outline-offset: 0;
}

/* :not(:placeholder-shown) stops an empty required field turning red on load */
input:invalid:not(:placeholder-shown) { border-color: var(--danger); }
input:valid:not(:placeholder-shown)   { border-color: var(--ok); }

/* The script's verdict must beat the browser's. :valid knows only `required` and
   the type, so to it 1234567890 is a good phone number; the script's rule, an
   Indian mobile, says it is not. input:valid:not(:placeholder-shown) is more
   specific than input.is-invalid, so .is-invalid repeats those pseudo-classes.
   [Corrected: without them, a field the script rejected kept a green border.] */
input.is-invalid, input.is-invalid:valid:not(:placeholder-shown),
select.is-invalid, textarea.is-invalid {
  border-color: var(--danger);
  background: #fff6f6;
}

::placeholder { color: #9aa4b2; }

/* Step 5: Style the error messages and the buttons */
.error {
  display: block;
  min-height: 1.2em;          /* reserve the space so the layout never jumps */
  margin: -.75rem 0 .75rem;
  color: var(--danger);
  font-size: .85rem;
}

button {
  padding: 10px 20px;
  border: 0;
  border-radius: 6px;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  transition: background .2s, transform .12s;
}

button[type="submit"] { background: var(--brand); color: #fff; }
button[type="submit"]:hover  { background: var(--brand-dark); }
button[type="submit"]:active { transform: translateY(1px); }
button[type="reset"] { background: #e8ecf2; color: var(--ink); }
button:disabled { background: #aeb6c2; color: #fff; cursor: not-allowed; }

/* Step 6: Respect the user's settings */
/* :focus-visible shows the ring for keyboard users but not on a mouse click */
:focus-visible { outline: 3px solid var(--ok); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* Visually hidden but still announced by screen readers. Used for table
   captions that would be redundant on screen. Note: display:none would hide
   it from assistive technology too, which is not what we want. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

5. Execution and Results

OUTPUT

opened 07_styled_form.html in Chromium, 900 px wide: "Experiment 7 — A CSS-styled student registration form"
  H1  Student registration
  elements: 2 p, 1 form, 10 input, 1 select, 2 button
  [screenshot 1: the whole page]

07_styled_form.html: screenshot 1 of 1, taken while the program ran

Corrected: the .is-invalid rule lost to input:valid:not(:placeholder-shown), which is more specific, so a field the script had rejected kept a green border while showing its error — in Experiment 11, a mobile number of 1234567890 looked accepted. The rule now repeats those pseudo-classes, and the stylesheet says why.

RESULT

The form is drawn with the stylesheet's colours, borders and spacing, and no field is red before anything has been typed.

Experiment 8 — Responsive page with Flexbox and Grid

1. Question

Create a responsive page with Flexbox and Grid.

2. Aim

Lay a page out with Flexbox and Grid so that it adapts to the screen's width.

3. Steps

In HTML, 08_responsive.html:

  1. Set up the head: the viewport meta, and the stylesheet.
  2. A navigation bar.
  3. The page header.
  4. A sidebar.
  5. The main content: cards, and a media object.
  6. The footer.

In CSS, 08_responsive.css:

  1. Define the colours and the gap.
  2. A navigation bar with Flexbox.
  3. A page layout with named Grid areas.
  4. A card grid that needs no media query.
  5. A media object with Flexbox.
  6. Change the layout at the breakpoints, for print and for reduced motion.

THE VIEWPORT, AND THE RULE

<meta name="viewport" content="width=device-width, initial-scale=1">

Without that meta tag the page is not responsive, whatever the CSS says: a phone pretends to be 980px wide and renders your desktop layout shrunk to illegibility.

Flexbox for one dimension, Grid for two — that is the whole rule.

The repeat(auto-fit, minmax(240px, 1fr)) line is worth memorising: it fits as many columns as will hold 240px each and stretches them to fill the row, reflowing at every width with no media queries at all. It is a complete responsive grid in one declaration.

4. Programme

In HTML, 08_responsive.html:

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head: the viewport meta, and the stylesheet -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 8 — Responsive page with Flexbox and Grid</title>
<link rel="stylesheet" href="08_responsive.css">
</head>
<body>

<!-- Step 2: A navigation bar -->
<nav class="nav">
  <span class="brand">Data Science</span>
  <ul>
    <li><a href="#home">Home</a></li>
    <li><a href="#courses">Courses</a></li>
    <li><a href="#labs">Labs</a></li>
    <li><a href="#contact">Contact</a></li>
  </ul>
</nav>

<div class="page">

  <!-- Step 3: The page header -->
  <header>
    <h1>Semester III</h1>
    <p>Resize the window. The navigation wraps, the sidebar moves below the
       content under 720px, and the cards reflow with no breakpoint of their
       own.</p>
  </header>

  <!-- Step 4: A sidebar -->
  <aside>
    <h2>Units</h2>
    <ul>
      <li>HTML</li>
      <li>CSS</li>
      <li>JavaScript</li>
      <li>Client-side scripting</li>
      <li>JSON and jQuery</li>
    </ul>
  </aside>

  <!-- Step 5: The main content: cards, and a media object -->
  <main>
    <h2>Courses</h2>
    <div class="cards">
      <article class="card">
        <h3>Course 5</h3>
        <p>Database Management Systems — SQL, normalisation, transactions.</p>
      </article>
      <article class="card">
        <h3>Course 6</h3>
        <p>Data Science with R — vectors, data frames, ggplot2, ARIMA.</p>
      </article>
      <article class="card">
        <h3>Course 7</h3>
        <p>Web Technologies — HTML, CSS, JavaScript, JSON and jQuery.</p>
      </article>
      <article class="card">
        <h3>Course 8</h3>
        <p>Data Mining — association rules, classification, clustering.</p>
      </article>
    </div>

    <h2>A flex media object</h2>
    <div class="media">
      <img src="images/campus.svg" alt="The college campus">
      <div class="body">
        <h3>Why mobile first?</h3>
        <p>Writing the small-screen layout as the default and adding
           complexity with <code>min-width</code> produces less CSS than the
           reverse, and degrades safely on devices you did not anticipate.</p>
      </div>
    </div>
  </main>

  <!-- Step 6: The footer -->
  <footer class="no-print">
    <p><small>Flexbox for one dimension, Grid for two. That is the whole
      rule.</small></p>
  </footer>

</div>

</body>
</html>

In CSS, 08_responsive.css:

/* Experiment 8 — A responsive page with Flexbox and Grid.
 *
 * Mobile-first: the base rules ARE the small-screen rules, and media queries
 * add complexity as the viewport grows. This produces less CSS than the
 * reverse and degrades safely on devices nobody anticipated.
 *
 * None of it works without <meta name="viewport"> in the HTML head.
 */

/* Step 1: Define the colours and the gap */
:root {
  --brand: #2b4c7e;
  --line:  #d6dbe4;
  --ink:   #1f2733;
  --gap:   1rem;
}

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  font-family: system-ui, sans-serif;
  line-height: 1.6;
  color: var(--ink);
  background: #f4f6f9;
}

/* Step 2: A navigation bar with Flexbox */

.nav {
  display: flex;
  flex-wrap: wrap;                 /* wrap instead of overflowing on a phone */
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  padding: .8rem 1rem;
  background: var(--brand);
  color: #fff;
}

.nav .brand { font-weight: 700; font-size: 1.1rem; }

.nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap);
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav a { color: #fff; text-decoration: none; padding: .2rem 0;
         border-bottom: 2px solid transparent; }
.nav a:hover, .nav a:focus-visible { border-bottom-color: #fff; }

/* Step 3: A page layout with named Grid areas */

.page {
  display: grid;
  gap: var(--gap);
  padding: var(--gap);
  max-width: 1100px;
  margin: 0 auto;

  /* mobile first: one column, and MAIN COMES BEFORE THE SIDEBAR so a phone
     user reads the content first */
  grid-template-areas:
    "head"
    "main"
    "side"
    "foot";
  grid-template-columns: 1fr;
}

.page > header { grid-area: head; }
.page > main   { grid-area: main; }
.page > aside  { grid-area: side; }
.page > footer { grid-area: foot; }

.page > * {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 1rem;
}

/* Step 4: A card grid that needs no media query */

.cards {
  display: grid;
  gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}

.card {
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 1rem;
  background: #fbfcfe;
}

.card h3 { margin-top: 0; color: var(--brand); }

/* auto-fit fits as many columns as will hold 240px and stretches them to
   fill the row. It reflows at every width with no breakpoints of our own. */

/* Step 5: A media object with Flexbox */

.media { display: flex; gap: var(--gap); flex-wrap: wrap; align-items: flex-start; }
.media img { width: 120px; height: 90px; object-fit: cover; border-radius: 6px;
             flex: 0 0 auto; }
.media .body { flex: 1 1 240px; }     /* grow, shrink, 240px basis */

/* Step 6: Change the layout at the breakpoints, for print and for reduced motion */

@media (min-width: 720px) {
  .page {
    grid-template-areas:
      "head head"
      "side main"
      "foot foot";
    grid-template-columns: 220px 1fr;
  }
}

@media (min-width: 1100px) {
  :root { --gap: 1.5rem; }
  .page { grid-template-columns: 260px 1fr; }
}

@media print {
  .nav, .no-print { display: none; }
  body { background: #fff; }
  .page > * { border: 0; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

5. Execution and Results

OUTPUT

opened 08_responsive.html in Chromium, 390 px wide: "Experiment 8 — Responsive page with Flexbox and Grid"
  page grid columns: 358px
  cards: 1 in the first row, of 4; the sidebar is below the content
  [screenshot 1: 390 px wide]
opened 08_responsive.html in Chromium, 800 px wide: "Experiment 8 — Responsive page with Flexbox and Grid"
  page grid columns: 220px 532px
  cards: 2 in the first row, of 4; the sidebar is beside the content
  [screenshot 2: 800 px wide]
opened 08_responsive.html in Chromium, 1200 px wide: "Experiment 8 — Responsive page with Flexbox and Grid"
  page grid columns: 260px 768px
  cards: 2 in the first row, of 4; the sidebar is beside the content
  [screenshot 3: 1200 px wide]

08_responsive.html: screenshot 1 of 3, taken while the program ran

08_responsive.html: screenshot 2 of 3, taken while the program ran

08_responsive.html: screenshot 3 of 3, taken while the program ran

Note that the mobile layout puts main above side, so a phone user reads the content before the sidebar. Named grid areas make that reordering a one-line change. At 1200 px the cards still come two to a row: their box is 734 px wide, and three cards of 240 px, with the two 24 px gaps used above 1100 px, need 768.

RESULT

At 390 px there is one column, with the sidebar under the content; at 800 and 1200 px the sidebar sits beside it, and the cards reflow on their own.

Experiment 9 — Hover effects and transitions

1. Question

Add hover effects and transitions to a page with CSS.

2. Aim

Animate elements on hover with CSS transitions, cheaply and considerately.

3. Steps

In HTML, 09_hover.html:

  1. Set up the head, and link the stylesheet.
  2. Buttons.
  3. Images that zoom.
  4. Cards whose caption slides up.
  5. A growing underline.
  6. A tooltip.
  7. A keyframe animation.

In CSS, 09_hover.css:

  1. Buttons: background, lift and shadow.
  2. Image zoom, cropped by the wrapper.
  3. A caption that slides up on hover.
  4. An underline that grows from the centre.
  5. A CSS-only tooltip.
  6. Keyframe animations.
  7. Respect the user's motion preference.

THREE POINTS EARN THE MARKS

Animate transform and opacity, not width, top or margin. The browser can composite transform and opacity changes on the GPU without recalculating layout; the others force a reflow on every frame and stutter.

overflow: hidden on the wrapper is what makes the zoom crop cleanly instead of the enlarged image spilling over its neighbours.

The prefers-reduced-motion block respects users who get motion sickness from animation. It is two lines and it is the difference between a page that is merely pretty and one that is considerate.

4. Programme

In HTML, 09_hover.html:

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head, and link the stylesheet -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 9 — Hover effects and transitions</title>
<link rel="stylesheet" href="09_hover.css">
</head>
<body>

<h1>Hover effects and transitions</h1>
<p>Every effect on this page is pure CSS — there is no JavaScript at all.</p>

<!-- Step 2: Buttons -->
<h2>1. Buttons — background, lift and shadow</h2>
<p>
  <button class="btn" type="button">Solid</button>
  <button class="btn outline" type="button">Outline</button>
</p>
<p><small><code>transition: background .25s, transform .25s, box-shadow .25s</code>
  — three properties, one declaration.</small></p>

<!-- Step 3: Images that zoom -->
<h2>2. Image zoom, cropped by the wrapper</h2>
<div class="thumbs">
  <div class="thumb"><img src="images/1.svg" alt="Placeholder image one"></div>
  <div class="thumb"><img src="images/2.svg" alt="Placeholder image two"></div>
  <div class="thumb"><img src="images/3.svg" alt="Placeholder image three"></div>
  <div class="thumb"><img src="images/4.svg" alt="Placeholder image four"></div>
</div>
<p><small><code>overflow: hidden</code> on the wrapper is what makes the zoom
  crop cleanly instead of spilling over its neighbours.</small></p>

<!-- Step 4: Cards whose caption slides up -->
<h2>3. A caption that slides up</h2>
<div class="thumbs">
  <figure class="card" style="margin:0">
    <img src="images/5.svg" alt="Placeholder image five">
    <figcaption class="caption">Slides up on hover</figcaption>
  </figure>
  <figure class="card" style="margin:0">
    <img src="images/6.svg" alt="Placeholder image six">
    <figcaption class="caption">rgba background, crisp text</figcaption>
  </figure>
</div>
<p><small>The overlay uses <code>rgba(0,0,0,.65)</code>, not
  <code>opacity</code>. <code>opacity</code> would fade the white text
  too.</small></p>

<!-- Step 5: A growing underline -->
<h2>4. An underline that grows from the centre</h2>
<p>An <a class="link" href="#4">animated link underline</a>, built with
  <code>::after</code> and a width transition.</p>

<!-- Step 6: A tooltip -->
<h2>5. A CSS-only tooltip</h2>
<p>The coefficient of determination
  <span class="tip" tabindex="0" data-tip="Proportion of variance explained">R&sup2;</span>
  is covered in Course 4.</p>
<p><small><code>content: attr(data-tip)</code> reads the text straight out of
  the HTML attribute, so one rule serves every tooltip on the page.</small></p>

<!-- Step 7: A keyframe animation -->
<h2>6. Keyframe animation</h2>
<p class="fade-up">This paragraph faded up when the page loaded.</p>
<p><button class="btn pulse" type="button">Hover to pulse</button></p>

<p><small><strong>Note the <code>prefers-reduced-motion</code> block</strong>
  at the end of the stylesheet. Two lines, and it is the difference between a
  page that is merely pretty and one that is considerate.</small></p>

</body>
</html>

In CSS, 09_hover.css:

/* Experiment 9 — Hover effects and transitions.
 *
 * Rule of thumb: animate `transform` and `opacity`. The browser can composite
 * those on the GPU without recalculating layout. Animating `width`, `height`,
 * `top` or `margin` forces a reflow on every frame and stutters.
 */

*, *::before, *::after { box-sizing: border-box; }

body {
  font-family: system-ui, sans-serif;
  line-height: 1.6;
  max-width: 860px;
  margin: 2rem auto;
  padding: 0 1rem;
  color: #1f2733;
  background: #f4f6f9;
}

h2 { color: #2b4c7e; margin-top: 2.5rem; }

/* Step 1: Buttons: background, lift and shadow */

.btn {
  display: inline-block;
  padding: 10px 20px;
  border: 0;
  border-radius: 6px;
  background: #2b4c7e;
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  transition: background .25s ease, transform .25s ease, box-shadow .25s ease;
}

.btn:hover  { background: #1f3a63; transform: translateY(-2px);
              box-shadow: 0 4px 12px rgba(31, 39, 51, .25); }
.btn:active { transform: translateY(0); box-shadow: none; }

.btn.outline { background: transparent; color: #2b4c7e;
               box-shadow: inset 0 0 0 2px #2b4c7e; }
.btn.outline:hover { background: #2b4c7e; color: #fff; }

/* Step 2: Image zoom, cropped by the wrapper */

.thumbs { display: grid; gap: 12px;
          grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }

.thumb {
  overflow: hidden;              /* THIS is what crops the zoom cleanly */
  border-radius: 8px;
  border: 1px solid #d6dbe4;
  background: #fff;
}

.thumb img {
  display: block;
  width: 100%;
  height: 130px;
  object-fit: cover;
  filter: grayscale(60%);
  transition: transform .4s ease, filter .4s ease;
}

.thumb:hover img { transform: scale(1.1); filter: grayscale(0); }

/* Step 3: A caption that slides up on hover */

.card {
  position: relative;            /* the anchor for the absolute caption */
  overflow: hidden;
  border-radius: 8px;
}

.card img { display: block; width: 100%; height: 180px; object-fit: cover; }

.card .caption {
  position: absolute;
  inset: auto 0 0 0;             /* top auto, right/bottom/left 0 */
  background: rgba(0, 0, 0, .65); /* rgba, NOT opacity — the text stays crisp */
  color: #fff;
  padding: .7rem 1rem;
  transform: translateY(100%);
  transition: transform .3s ease;
}

.card:hover .caption,
.card:focus-within .caption { transform: translateY(0); }

/* Step 4: An underline that grows from the centre */

.link {
  position: relative;
  color: #2b4c7e;
  text-decoration: none;
  font-weight: 600;
}

.link::after {
  content: "";                   /* mandatory on ::before / ::after */
  position: absolute;
  left: 50%; bottom: -3px;
  width: 0; height: 2px;
  background: #2b4c7e;
  transition: width .25s ease, left .25s ease;
}

.link:hover::after,
.link:focus-visible::after { width: 100%; left: 0; }

/* Step 5: A CSS-only tooltip */

.tip {
  position: relative;
  border-bottom: 1px dotted #888;
  cursor: help;
}

.tip::after {
  content: attr(data-tip);       /* pull the text out of the HTML attribute */
  position: absolute;
  bottom: 125%; left: 50%;
  transform: translateX(-50%);   /* centre on the anchor, whatever the width */
  background: #1f2733;
  color: #fff;
  padding: 6px 10px;
  border-radius: 4px;
  white-space: nowrap;
  font-size: .85rem;
  opacity: 0;
  visibility: hidden;            /* toggled with opacity so it is unclickable */
  transition: opacity .25s ease;
}

.tip:hover::after, .tip:focus::after { opacity: 1; visibility: visible; }

/* Step 6: Keyframe animations */

@keyframes fade-up {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

.fade-up { animation: fade-up .5s ease-out both; }

@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.06); }
}

.pulse:hover { animation: pulse .6s ease-in-out infinite; }

/* Step 7: Respect the user's motion preference */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition: none !important;
    animation: none !important;
  }
}

5. Execution and Results

OUTPUT

opened 09_hover.html in Chromium, 900 px wide: "Experiment 9 — Hover effects and transitions"
button:  transform none -> matrix(1, 0, 0, 1, 0, -2)  (lifted 2 px)
  [screenshot 1: hovering over the first button]
image:   filter grayscale(0.6) -> grayscale(0), transform none -> matrix(1.1, 0, 0, 1.1, 0, 0)
  [screenshot 2: hovering over the first image]
caption: transform matrix(1, 0, 0, 1, 0, 47.9688) -> matrix(1, 0, 0, 1, 0, 0)  (slid up into view)
  [screenshot 3: hovering over the first card]
tooltip: opacity of its ::after is 1
  [screenshot 4: hovering over the tooltip]

09_hover.html: screenshot 1 of 4, taken while the program ran

09_hover.html: screenshot 2 of 4, taken while the program ran

09_hover.html: screenshot 3 of 4, taken while the program ran

09_hover.html: screenshot 4 of 4, taken while the program ran

Note that rgba() fades only the caption's background; opacity there would fade the white text too.

RESULT

On hover the button lifts 2 px, the image loses its grey and scales by 1.1, the caption slides up into view, and the tooltip appears.

Experiment 10 — JavaScript string operations

1. Question

Write JavaScript string operations: reverse a string, take a substring, and count the vowels.

2. Aim

Operate on strings in JavaScript, and show the results live on a page.

3. Steps

In HTML, 10_string_ops.html:

  1. Set up the head, and link the stylesheet.
  2. A text box for the string.
  3. Show every result for what is typed.
  4. Update on every keystroke.

In JavaScript, 10_string_ops.js:

  1. Reverse a string.
  2. Count the vowels, consonants and words.
  3. Test for a palindrome, title-case, and count each character.
  4. Find the longest word.
  5. Put everything together.

THREE THINGS TO NOTICE

[...s] splits by code point, while s.split("") splits by UTF-16 code unit. For plain ASCII they agree; for an emoji or an accented character split("") tears the character in half and the reversal produces garbage.

(s.match(…) || []) is not optional. match returns null when nothing matches, not an empty array, and null.length throws.

s.trim().split(/\s+/) on an empty string gives [""], whose length is 1 — so a word count needs the explicit empty check, or every blank input reports one word.

4. Programme

In HTML, 10_string_ops.html:

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head, and link the stylesheet -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 10 — String operations</title>
<link rel="stylesheet" href="07_styled_form.css">
<style>
  table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
  th, td { border: 1px solid #d6dbe4; padding: 8px 10px; text-align: left; }
  th { background: #eef2f7; width: 40%; }
  tbody tr:nth-child(even) { background: #fafbfd; }
  code { background: #eef2f7; padding: 1px 5px; border-radius: 3px; }
</style>
</head>
<body>

<h1>String operations</h1>

<!-- Step 2: A text box for the string -->
<form id="f" onsubmit="return false">
  <fieldset>
    <legend>Input</legend>
    <label for="text">Enter some text</label>
    <input type="text" id="text" name="text" value="Data Science"
           placeholder="Type anything">
  </fieldset>
  <table>
    <caption class="sr-only">Computed string statistics</caption>
    <tbody id="out"></tbody>
  </table>
</form>

<script type="module">
  import { stats } from "./10_string_ops.js";

  const input = document.getElementById("text");
  const out   = document.getElementById("out");

  const LABELS = {
    length: "Length", words: "Words", vowels: "Vowels",
    consonants: "Consonants", digits: "Digits",
    upper: "Uppercase", lower: "Lowercase", title: "Title Case",
    reversed: "Reversed", firstFour: "slice(0, 4)", lastSeven: "slice(-7)",
    isPalindrome: "Palindrome?", longestWord: "Longest word"
  };

  // Step 3: Show every result for what is typed
  function render() {
    const s = stats(input.value);
    out.textContent = "";
    for (const [key, label] of Object.entries(LABELS)) {
      const tr = document.createElement("tr");
      const th = document.createElement("th");
      const td = document.createElement("td");
      th.scope = "row";
      th.textContent = label;
      td.textContent = String(s[key]);      // textContent, never innerHTML
      tr.append(th, td);
      out.append(tr);
    }
  }

  // Step 4: Update on every keystroke
  input.addEventListener("input", render);  // live, on every keystroke
  render();
</script>

</body>
</html>

In JavaScript, 10_string_ops.js:

/* Experiment 10 — JavaScript string operations.
 *
 * Reverse, substring, count vowels — plus the statistics a viva usually asks
 * for on top. Every function is pure: input in, value out, no DOM. That is
 * what lets tools/run_web_labs.js assert them without a browser.
 */

// Step 1: Reverse a string
/** Reverse a string.
 *
 * [...s] splits by CODE POINT; s.split("") splits by UTF-16 code unit. For
 * plain ASCII they agree, but split("") tears an emoji or a surrogate pair in
 * half and the reversal produces garbage.
 */
export function reverse(s) {
  return [...s].reverse().join("");
}

// Step 2: Count the vowels, consonants and words
export function countVowels(s) {
  return [...s.toLowerCase()].filter(c => "aeiou".includes(c)).length;
}

export function countConsonants(s) {
  return (s.match(/[b-df-hj-np-tv-z]/gi) || []).length;
}

/** Word count. s.trim().split(/\s+/) on an empty string gives [""], whose
 *  length is 1 — so a blank input would otherwise report one word. */
export function countWords(s) {
  const t = s.trim();
  return t === "" ? 0 : t.split(/\s+/).length;
}

// Step 3: Test for a palindrome, title-case, and count each character
export function isPalindrome(s) {
  const clean = s.toLowerCase().replace(/[^a-z0-9]/g, "");
  return clean.length > 0 && clean === reverse(clean);
}

export function titleCase(s) {
  return s.toLowerCase().replace(/\b[a-z]/g, c => c.toUpperCase());
}

/** Frequency of each character, ignoring case and whitespace. */
export function charFrequency(s) {
  const freq = {};
  for (const c of s.toLowerCase()) {
    if (/\s/.test(c)) continue;
    freq[c] = (freq[c] || 0) + 1;      // (freq[c] || 0) handles the first hit
  }
  return freq;
}

// Step 4: Find the longest word
export function longestWord(s) {
  const words = s.trim() === "" ? [] : s.trim().split(/\s+/);
  return words.reduce((best, w) => (w.length > best.length ? w : best), "");
}

// Step 5: Put everything together
/** Everything at once — what the browser page displays. */
export function stats(s) {
  return {
    length:      s.length,
    words:       countWords(s),
    vowels:      countVowels(s),
    consonants:  countConsonants(s),
    digits:      (s.match(/\d/g) || []).length,
    upper:       s.toUpperCase(),
    lower:       s.toLowerCase(),
    title:       titleCase(s),
    reversed:    reverse(s),
    firstFour:   s.slice(0, 4),
    lastSeven:   s.slice(-7),
    isPalindrome: isPalindrome(s),
    longestWord: longestWord(s)
  };
}

5. Execution and Results

OUTPUT

opened 10_string_ops.html in Chromium, 900 px wide: "Experiment 10 — String operations"

typed 'Data Science'; the page shows:
  Length    12
  Words 2
  Vowels    5
  Consonants    6
  Digits    0
  Uppercase DATA SCIENCE
  Lowercase data science
  Title Case    Data Science
  Reversed  ecneicS ataD
  slice(0, 4)   Data
  slice(-7) Science
  Palindrome?   false
  Longest word  Science
  [screenshot 1: after typing 'Data Science']

typed 'A man, a plan, a canal: Panama'; the page shows:
  Length    30
  Words 7
  Vowels    10
  Consonants    11
  Digits    0
  Uppercase A MAN, A PLAN, A CANAL: PANAMA
  Lowercase a man, a plan, a canal: panama
  Title Case    A Man, A Plan, A Canal: Panama
  Reversed  amanaP :lanac a ,nalp a ,nam A
  slice(0, 4)   A ma
  slice(-7) Panama
  Palindrome?   true
  Longest word  canal:
  [screenshot 2: after typing 'A man, a plan, a canal: Panama']

10_string_ops.html: screenshot 1 of 2, taken while the program ran

10_string_ops.html: screenshot 2 of 2, taken while the program ran

Asserted by the runner: reverse("Data Science") === "ecneicS ataD", countVowels("Data Science") === 5, isPalindrome("A man, a plan, a canal: Panama") === true, and stats("").words === 0. The longest word of the second string is "canal:", colon and all: words are split on spaces only.

RESULT

"Data Science" has 12 characters, 2 words and 5 vowels, and reverses to "ecneicS ataD"; "A man, a plan, a canal: Panama" is a palindrome.

Experiment 11 — Form validation

1. Question

Validate a form in JavaScript: the email's format, the password's length and the required fields.

2. Aim

Check a form's fields in JavaScript before it is sent, and show each error beside its field.

3. Steps

In HTML, 11_validation.html:

  1. Set up the head, and link the stylesheet.
  2. The registration form, with a place for each error.
  3. Load the code that checks it.

In JavaScript, 11_validation.js:

  1. Write a rule for each field.
  2. Check every rule, and the two that compare fields.
  3. Check that a date is real, and find an age.

In JavaScript, 11_validation_wire.js:

  1. Show or clear a field's error.
  2. Read the form's values.
  3. Check everything on submit.
  4. Check a field again once it has been left.

THE DETAIL MOST OFTEN MISSED

The HTML half wires it up, and its submit handler is the part that matters:

form.addEventListener("submit", e => {
  const errors = validate(readValues());
  for (const id of [...Object.keys(RULES), "confirm", "terms"])
    setFieldError(form.elements[id], errors[id] || "");
  if (Object.keys(errors).length) {
    e.preventDefault();                          // THIS is what cancels submit
    form.querySelector(".is-invalid")?.focus();  // move focus to the first problem
  }
});

FormData omits unchecked checkboxes entirely — the key is simply absent, not false — so the terms value has to be read from .checked separately. That one line is the most commonly missed detail in this experiment.

And say it in the viva: this is a convenience, not a control. The server validates again.

4. Programme

In HTML, 11_validation.html:

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head, and link the stylesheet -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 11 — JavaScript form validation</title>
<link rel="stylesheet" href="07_styled_form.css">
</head>
<body>

<h1>Form validation</h1>
<p>Leave a field blank or wrong and press <strong>Register</strong>. Errors
   appear inline, and focus jumps to the first problem.</p>

<!-- Step 2: The registration form, with a place for each error -->
<form id="reg" action="/register" method="post" novalidate>

  <fieldset>
    <legend>Details</legend>

    <label for="name">Full name <span aria-hidden="true">*</span></label>
    <input type="text" id="name" name="name" required
           placeholder="Asha Kumari" autocomplete="name"
           aria-describedby="name-error">
    <span id="name-error" class="error" role="alert"></span>

    <label for="email">Email <span aria-hidden="true">*</span></label>
    <input type="email" id="email" name="email" required
           placeholder="asha@nri.ac.in" autocomplete="email"
           aria-describedby="email-error">
    <span id="email-error" class="error" role="alert"></span>

    <label for="mobile">Mobile <span aria-hidden="true">*</span></label>
    <input type="tel" id="mobile" name="mobile" required
           placeholder="9876543210" aria-describedby="mobile-error">
    <span id="mobile-error" class="error" role="alert"></span>
  </fieldset>

  <fieldset>
    <legend>Password</legend>

    <label for="password">Password <span aria-hidden="true">*</span></label>
    <input type="password" id="password" name="password" required
           placeholder="8+ chars, mixed case, digit, symbol"
           autocomplete="new-password" aria-describedby="password-error">
    <span id="password-error" class="error" role="alert"></span>

    <label for="confirm">Confirm password <span aria-hidden="true">*</span></label>
    <input type="password" id="confirm" name="confirm" required
           placeholder="Type it again" autocomplete="new-password"
           aria-describedby="confirm-error">
    <span id="confirm-error" class="error" role="alert"></span>
  </fieldset>

  <input type="checkbox" id="terms" name="terms" required
         aria-describedby="terms-error">
  <label for="terms">I accept the terms <span aria-hidden="true">*</span></label>
  <span id="terms-error" class="error" role="alert"></span>

  <p><button type="submit">Register</button>
     <button type="reset">Clear</button></p>

  <p id="status" role="status" aria-live="polite"></p>
</form>

<p><small><strong>This is not security.</strong> It runs on the user's machine
  and can be bypassed by disabling JavaScript, editing the DOM, or sending the
  request with <code>curl</code>. The server must validate every field
  again.</small></p>

<!-- Step 3: Load the code that checks it -->
<script type="module" src="11_validation_wire.js"></script>

</body>
</html>

In JavaScript, 11_validation.js:

/* Experiment 11 — Form validation.
 *
 * The rules are DATA, not a wall of if-statements, so adding a field is one
 * line and the same array drives both submit-time and live validation.
 *
 * IMPORTANT: none of this is security. It runs on the user's machine and can
 * be bypassed by disabling JavaScript, editing the DOM, or sending the request
 * with curl. The server must validate every field again.
 */

// Step 1: Write a rule for each field
export const RULES = {
  name: {
    test: v => v.trim().length >= 3 && v.trim().length <= 50,
    msg:  "Name must be 3 to 50 characters"
  },
  email: {
    // A plausible shape, not an RFC-complete pattern. The only real proof an
    // address exists is sending a confirmation message to it.
    test: v => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v.trim()),
    msg:  "Enter a valid email address"
  },
  mobile: {
    test: v => /^[6-9]\d{9}$/.test(v.trim()),
    msg:  "Enter a 10-digit Indian mobile number"
  },
  password: {
    // Four lookaheads assert lowercase, uppercase, digit and symbol in any
    // order without consuming anything; .{8,} then does the matching.
    test: v => /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/.test(v),
    msg:  "8+ characters with upper, lower, digit and symbol"
  }
};

// Step 2: Check every rule, and the two that compare fields
/** Validate a plain object of values. Returns { field: message } — empty when
 *  everything passed. */
export function validate(values) {
  const errors = {};

  for (const [field, rule] of Object.entries(RULES))
    if (!rule.test(values[field] ?? "")) errors[field] = rule.msg;

  // Rules that compare two fields cannot be expressed as a pattern.
  if (values.password !== values.confirm)
    errors.confirm = "Passwords do not match";

  // A checkbox is read from .checked, never .value.
  if (!values.terms)
    errors.terms = "You must accept the terms";

  return errors;
}

// Step 3: Check that a date is real, and find an age
/** A real calendar date. 2026-02-30 matches the shape but does not exist:
 *  new Date(2026, 1, 30) rolls over to 2 March, so comparing the components
 *  back is what proves the date was real. */
export function isRealDate(s) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return false;
  const [y, m, d] = s.split("-").map(Number);
  const dt = new Date(y, m - 1, d);
  return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d;
}

/** Age in whole years, given an ISO date of birth. */
export function ageOn(dobStr, on = new Date()) {
  const dob = new Date(dobStr);
  let age = on.getFullYear() - dob.getFullYear();
  const m = on.getMonth() - dob.getMonth();
  if (m < 0 || (m === 0 && on.getDate() < dob.getDate())) age--;
  return age;
}

In JavaScript, 11_validation_wire.js:

/* Experiment 11 — the DOM half. Wires 11_validation.js to the form in
 * 11_validation.html (styled by 07_styled_form.css). Kept separate so the rules
 * stay testable. [Corrected: this named the form as in 07_styled_form.html.] */

import { RULES, validate } from "./11_validation.js";

const form = document.getElementById("reg");

// Step 1: Show or clear a field's error
function setFieldError(input, message) {
  if (!input) return;
  const box = document.getElementById(input.id + "-error");
  input.classList.toggle("is-invalid", Boolean(message));
  input.setAttribute("aria-invalid", message ? "true" : "false");
  if (box) box.textContent = message || "";      // textContent, never innerHTML
}

// Step 2: Read the form's values
function readValues() {
  const values = Object.fromEntries(new FormData(form));
  // FormData OMITS unchecked checkboxes entirely — the key is absent, not
  // false — so the checkbox has to be read from .checked separately.
  values.terms = form.elements.terms.checked;
  return values;
}

// Step 3: Check everything on submit
form.addEventListener("submit", e => {
  const errors = validate(readValues());
  for (const id of [...Object.keys(RULES), "confirm", "terms"])
    setFieldError(form.elements[id], errors[id] || "");

  if (Object.keys(errors).length) {
    e.preventDefault();                          // THIS is what cancels submit
    form.querySelector(".is-invalid")?.focus();  // move focus to the first problem
    document.getElementById("status").textContent = "";
  } else {
    e.preventDefault();                          // demo only: nothing to POST to
    document.getElementById("status").textContent = "All fields valid.";
  }
});

// Step 4: Check a field again once it has been left
// Live feedback, but only after the user has left the field once. Validating
// from the first character shows "invalid email" while they are still typing
// the first letter, which is hostile.
for (const [field, rule] of Object.entries(RULES)) {
  const input = form.elements[field];
  if (!input) continue;
  let touched = false;
  const run = () => setFieldError(input, rule.test(input.value) ? "" : rule.msg);
  input.addEventListener("blur",  () => { touched = true; run(); });
  input.addEventListener("input", () => { if (touched) run(); });
}

5. Execution and Results

OUTPUT

opened 11_validation.html in Chromium, 900 px wide: "Experiment 11 — JavaScript form validation"
submitted empty:
  name      Name must be 3 to 50 characters
  email     Enter a valid email address
  mobile    Enter a 10-digit Indian mobile number
  password  8+ characters with upper, lower, digit and symbol
  terms     You must accept the terms
  [screenshot 1: submitted empty]

submitted with a bad email, mobile and password, the passwords not matching, and the terms not accepted:
  email     Enter a valid email address
  mobile    Enter a 10-digit Indian mobile number
  password  8+ characters with upper, lower, digit and symbol
  confirm   Passwords do not match
  terms     You must accept the terms
  [screenshot 2: submitted with mistakes]

submitted correctly: errors {}, status 'All fields valid.'
  [screenshot 3: submitted correctly]

11_validation.html: screenshot 1 of 3, taken while the program ran

11_validation.html: screenshot 2 of 3, taken while the program ran

11_validation.html: screenshot 3 of 3, taken while the program ran

The runner asserts that asha@ is rejected and asha@nri.ac.in accepted; that 9876543210 passes and 1234567890 fails (an Indian mobile starts 6–9); that Passw0rd! passes and password fails; and that mismatched passwords produce exactly the confirm error. The driver tries the same values in the page.

RESULT

Each wrong field gets its message and a red border; with every field right, the page reports "All fields valid."

Experiment 12 — Time-based greeting

1. Question

Show a greeting that changes with the time of day.

2. Aim

Greet by the hour in JavaScript, in a way that can be tested at every hour.

3. Steps

In HTML, 12_greeting.html:

  1. Set up the head: charset, viewport, title and styles.
  2. A card for the greeting and the clock.
  3. Greet by the hour, with the name.
  4. Tick the clock every second.

In JavaScript, 12_greeting.js:

  1. Choose the greeting from the hour.
  2. Give a class for each period of the day.
  3. Add the name.
  4. Format the time.

THE WHOLE TRICK

Taking hour as a parameter with a default is the whole trick. A function that reads the clock internally can only be tested by waiting until 9 am; this one is asserted at every boundary — 0, 11, 12, 16, 17, 20, 21, 23 — in a fraction of a second, and still behaves identically when called with no arguments in the page.

That is a general lesson, not a JavaScript one: push the unpredictable input to the edge of the function, and the logic inside becomes testable. The same idea makes Python for Data Analysis and Visualization's data pipelines testable.

4. Programme

In HTML, 12_greeting.html:

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head: charset, viewport, title and styles -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 12 — Time-based greeting</title>
<style>
  *, *::before, *::after { box-sizing: border-box; }
  body { font-family: system-ui, sans-serif; line-height: 1.6; margin: 0;
         min-height: 100vh; display: grid; place-items: center;
         transition: background .6s ease, color .6s ease; }
  .card { text-align: center; padding: 2rem 3rem; border-radius: 12px;
          background: rgba(255,255,255,.85); color: #1f2733;
          box-shadow: 0 6px 24px rgba(0,0,0,.15); }
  #greet { font-size: 2rem; margin: 0 0 .3rem; }
  #clock { font-family: ui-monospace, "Courier New", monospace;
           font-size: 1.4rem; color: #444; }
  label { display: block; margin-top: 1.2rem; font-weight: 600; }
  input, select { font: inherit; padding: 8px 10px; margin-top: .3rem;
                  border: 1px solid #ccd3dd; border-radius: 6px; }
  body.morning   { background: linear-gradient(#ffe9b0, #ffd27f); }
  body.afternoon { background: linear-gradient(#cfe8ff, #9ecbff); }
  body.evening   { background: linear-gradient(#ffc8a2, #f0876a); }
  body.night     { background: linear-gradient(#28324a, #131a2b); color: #eef; }
</style>
</head>
<body>

<!-- Step 2: A card for the greeting and the clock -->
<div class="card">
  <p id="greet">…</p>
  <p id="clock">--:--:--</p>

  <label for="name">Your name</label>
  <input type="text" id="name" placeholder="Asha">

  <label for="hour">Pretend the hour is</label>
  <select id="hour">
    <option value="">use the real clock</option>
  </select>
</div>

<script type="module">
  import { personalGreeting, periodClass, formatTime } from "./12_greeting.js";

  const greetEl = document.getElementById("greet");
  const clockEl = document.getElementById("clock");
  const nameEl  = document.getElementById("name");
  const hourEl  = document.getElementById("hour");

  for (let h = 0; h < 24; h++) {
    const o = document.createElement("option");
    o.value = String(h);
    o.textContent = String(h).padStart(2, "0") + ":00";
    hourEl.append(o);
  }

  // Step 3: Greet by the hour, with the name
  function render() {
    const override = hourEl.value === "" ? undefined : Number(hourEl.value);
    const hour = override ?? new Date().getHours();
    greetEl.textContent = personalGreeting(nameEl.value, hour);
    document.body.className = periodClass(hour);
  }

  nameEl.addEventListener("input", render);
  hourEl.addEventListener("change", render);

  // Step 4: Tick the clock every second
  // setInterval for a clock is correct — it is polling, not animation.
  // requestAnimationFrame would run 60 times a second to update once.
  setInterval(() => {
    clockEl.textContent = formatTime();
    if (hourEl.value === "") render();
  }, 1000);

  render();
  clockEl.textContent = formatTime();
</script>

</body>
</html>

In JavaScript, 12_greeting.js:

/* Experiment 12 — A greeting that changes with the time of day.
 *
 * The whole trick is the default parameter. A function that reads the clock
 * internally can only be tested by waiting until 9 a.m.; this one is asserted
 * at every boundary in a fraction of a second, and still behaves identically
 * when called with no arguments in the page.
 *
 * The general lesson: push the unpredictable input to the EDGE of the
 * function, and the logic inside becomes testable.
 */

// Step 1: Choose the greeting from the hour
export function greeting(hour = new Date().getHours()) {
  if (!Number.isInteger(hour) || hour < 0 || hour > 23)
    throw new RangeError("hour must be an integer from 0 to 23");

  if (hour < 12) return "Good morning";
  if (hour < 17) return "Good afternoon";
  if (hour < 21) return "Good evening";
  return "Good night";
}

// Step 2: Give a class for each period of the day
/** A CSS class per period, so the page can restyle itself too. */
export function periodClass(hour = new Date().getHours()) {
  return { "Good morning": "morning", "Good afternoon": "afternoon",
           "Good evening": "evening", "Good night": "night" }[greeting(hour)];
}

// Step 3: Add the name
/** "Good morning, Asha!" — with a sensible fallback for an empty name. */
export function personalGreeting(name, hour = new Date().getHours()) {
  const who = (name ?? "").trim();
  return who === "" ? `${greeting(hour)}!` : `${greeting(hour)}, ${who}!`;
}

// Step 4: Format the time
/** 24-hour clock formatted as "09:05:03". padStart is why this never shows
 *  "9:5:3". */
export function formatTime(d = new Date()) {
  const p = n => String(n).padStart(2, "0");
  return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}

5. Execution and Results

OUTPUT

opened 12_greeting.html in Chromium, 900 px wide: "Experiment 12 — Time-based greeting"
at the real (fixed) clock: 'Good morning!', the clock shows 09:00:00
  hour  9:00 -> 'Good morning, Asha!'  (body class 'morning')
  [screenshot 1: pretending the hour is 9:00]
  hour 14:00 -> 'Good afternoon, Asha!'  (body class 'afternoon')
  [screenshot 2: pretending the hour is 14:00]
  hour 19:00 -> 'Good evening, Asha!'  (body class 'evening')
  [screenshot 3: pretending the hour is 19:00]
  hour 22:00 -> 'Good night, Asha!'  (body class 'night')
  [screenshot 4: pretending the hour is 22:00]

12_greeting.html: screenshot 1 of 4, taken while the program ran

12_greeting.html: screenshot 2 of 4, taken while the program ran

12_greeting.html: screenshot 3 of 4, taken while the program ran

12_greeting.html: screenshot 4 of 4, taken while the program ran

The page's own "Pretend the hour is" list does in the browser what the parameter does in the runner.

RESULT

At 09:00 the page says "Good morning!"; with a name and the hour set to 14, 19 and 22, it says good afternoon, evening and night, and restyles itself for each.

Experiment 13 — Array and object manipulation

1. Question

Manipulate an array of objects in JavaScript: add, delete, sort and search.

2. Aim

Keep a list of students in an array, and change it only through pure functions.

3. Steps

In HTML, 13_arrays.html:

  1. Set up the head, and link the stylesheet.
  2. A form to add a student, a search box and a table.
  3. Draw the table and the summary.
  4. Add a student.
  5. Delete a student.
  6. Search, and sort by a column.

In JavaScript, 13_arrays.js:

  1. Start with five students.
  2. Add, remove and update, each returning a new array.
  3. Sort by any key.
  4. Search, find and group.
  5. Summarise.

PURE FUNCTIONS, AND THE COMPARATOR

Every function is pure: it takes the list and returns a new one rather than mutating in place. That is why addStudent spreads instead of pushing, and why sortBy copies with [...list] before sorting — sort mutates, and a sort that silently reorders the caller's array is a bug that surfaces three functions away.

The comparator branches on type because sort() with no comparator compares as strings, so [10, 9, 100] sorts to [10, 100, 9]. Numbers need x - y; strings need localeCompare, which also handles accents correctly.

4. Programme

In HTML, 13_arrays.html:

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head, and link the stylesheet -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 13 — Arrays and objects</title>
<link rel="stylesheet" href="07_styled_form.css">
<style>
  table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
  th, td { border: 1px solid #d6dbe4; padding: 8px 10px; text-align: left; }
  thead th { background: #eef2f7; cursor: pointer; user-select: none; }
  thead th:hover { background: #dde5ef; }
  tbody tr:nth-child(even) { background: #fafbfd; }
  .del { background: #c92a2a; color: #fff; border: 0; border-radius: 4px;
         padding: 3px 9px; cursor: pointer; font: inherit; font-size: .85rem; }
  #summary { background: #eef2f7; border-radius: 6px; padding: .8rem 1rem;
             margin-top: 1rem; }
  .row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: end; }
  .row > div { flex: 1 1 120px; }
</style>
</head>
<body>

<h1>Arrays and objects</h1>

<!-- Step 2: A form to add a student, a search box and a table -->
<form id="app" onsubmit="return false">
  <fieldset>
    <legend>Add a student</legend>
    <div class="row">
      <div><label for="roll">Roll</label>
           <input type="number" id="roll" name="roll" min="1"></div>
      <div><label for="name">Name</label>
           <input type="text" id="name" name="name"></div>
      <div><label for="marks">Marks</label>
           <input type="number" id="marks" name="marks" min="0" max="100"></div>
      <div><label for="dept">Dept</label>
           <select id="dept" name="dept"><option>DS</option><option>Stats</option></select></div>
      <div><button type="button" id="add">Add</button></div>
    </div>
    <p id="err" class="error" role="alert"></p>
  </fieldset>

  <fieldset>
    <legend>Search</legend>
    <label for="q">Filter by name, roll or department</label>
    <input type="search" id="q" name="q" placeholder="Type to filter">
  </fieldset>

  <table id="table">
    <thead>
      <tr>
        <th data-key="roll"  scope="col">Roll</th>
        <th data-key="name"  scope="col">Name</th>
        <th data-key="marks" scope="col">Marks</th>
        <th data-key="dept"  scope="col">Dept</th>
        <th scope="col">Action</th>
      </tr>
    </thead>
    <tbody id="body"></tbody>
  </table>

  <div id="summary"></div>
</form>

<p><small>Click a column heading to sort. The delete buttons use
  <strong>event delegation</strong> — one listener on the table body, so rows
  added later work with no extra wiring.</small></p>

<script type="module">
  import { STUDENTS, addStudent, removeStudent, sortBy, search, summary }
    from "./13_arrays.js";

  let list = [...STUDENTS];
  let sortKey = "roll", sortDesc = false;

  const body = document.getElementById("body");
  const err  = document.getElementById("err");

  // Step 3: Draw the table and the summary
  function render() {
    const rows = sortBy(search(list, document.getElementById("q").value),
                        sortKey, sortDesc);
    body.textContent = "";
    const frag = document.createDocumentFragment();
    for (const s of rows) {
      const tr = document.createElement("tr");
      for (const v of [s.roll, s.name, s.marks, s.dept]) {
        const td = document.createElement("td");
        td.textContent = String(v);        // textContent, never innerHTML
        tr.append(td);
      }
      const td = document.createElement("td");
      const btn = document.createElement("button");
      btn.type = "button";
      btn.className = "del";
      btn.dataset.roll = String(s.roll);
      btn.textContent = "Delete";
      td.append(btn);
      tr.append(td);
      frag.append(tr);
    }
    body.append(frag);

    const s = summary(rows);
    document.getElementById("summary").textContent = s.count === 0
      ? "No students match."
      : `${s.count} students · average ${s.average} · top ${s.top} · ${s.pass} passed`;
  }

  // Step 4: Add a student
  document.getElementById("add").addEventListener("click", () => {
    err.textContent = "";
    try {
      list = addStudent(list, {
        roll:  Number(document.getElementById("roll").value),
        name:  document.getElementById("name").value.trim() || "Unnamed",
        marks: Number(document.getElementById("marks").value),
        dept:  document.getElementById("dept").value
      });
      render();
    } catch (e) {
      err.textContent = e.message;
    }
  });

  // Step 5: Delete a student
  // delegation: one listener, works for rows added later
  body.addEventListener("click", e => {
    const btn = e.target.closest(".del");
    if (!btn) return;
    list = removeStudent(list, Number(btn.dataset.roll));
    render();
  });

  // Step 6: Search, and sort by a column
  document.getElementById("q").addEventListener("input", render);

  document.querySelectorAll("thead th[data-key]").forEach(th => {
    th.addEventListener("click", () => {
      const key = th.dataset.key;
      sortDesc = key === sortKey ? !sortDesc : false;
      sortKey = key;
      render();
    });
  });

  render();
</script>

</body>
</html>

In JavaScript, 13_arrays.js:

/* Experiment 13 — Manipulate arrays and objects: add, delete, sort, search.
 *
 * Every function here is PURE: it takes the list and returns a new one rather
 * than mutating in place. That is why addStudent spreads instead of pushing,
 * and why sortBy copies with [...list] before sorting — Array.prototype.sort
 * mutates, and a sort that silently reorders the caller's array is a bug that
 * surfaces three functions away from its cause.
 */

// Step 1: Start with five students
export const STUDENTS = [
  { roll: 21, name: "Asha",  marks: 72, dept: "DS"    },
  { roll: 22, name: "Ravi",  marks: 45, dept: "DS"    },
  { roll: 23, name: "Meena", marks: 91, dept: "Stats" },
  { roll: 24, name: "Kiran", marks: 66, dept: "DS"    },
  { roll: 25, name: "Bhanu", marks: 38, dept: "Stats" }
];

// Step 2: Add, remove and update, each returning a new array
export function addStudent(list, student) {
  if (list.some(s => s.roll === student.roll))
    throw new Error(`Roll ${student.roll} already exists`);
  return [...list, student];
}

export const removeStudent = (list, roll) => list.filter(s => s.roll !== roll);

export function updateStudent(list, roll, changes) {
  if (!list.some(s => s.roll === roll))
    throw new Error(`Roll ${roll} not found`);
  return list.map(s => (s.roll === roll ? { ...s, ...changes } : s));
}

// Step 3: Sort by any key
/** Sort by any key, ascending or descending.
 *
 * The comparator branches on type because sort() with no comparator compares
 * as STRINGS: [10, 9, 100].sort() gives [10, 100, 9]. Numbers need x - y;
 * strings need localeCompare, which also orders accented letters correctly.
 */
export const sortBy = (list, key, desc = false) =>
  [...list].sort((a, b) => {
    const [x, y] = desc ? [b[key], a[key]] : [a[key], b[key]];
    return typeof x === "string" ? x.localeCompare(y) : x - y;
  });

// Step 4: Search, find and group
export function search(list, term) {
  const t = String(term).trim().toLowerCase();
  if (t === "") return list;
  return list.filter(s => s.name.toLowerCase().includes(t)
                       || String(s.roll).includes(t)
                       || s.dept.toLowerCase().includes(t));
}

export const findByRoll = (list, roll) => list.find(s => s.roll === roll) ?? null;

/** Group into { dept: [names] } — the JavaScript equivalent of SQL's GROUP BY
 *  and of Course 9's df.groupby("dept"). */
export function groupBy(list, key) {
  const out = {};
  for (const item of list) (out[item[key]] ||= []).push(item);
  return out;
}

// Step 5: Summarise
export function summary(list) {
  if (list.length === 0) return { count: 0, average: null, top: null, pass: 0 };
  const total = list.reduce((sum, s) => sum + s.marks, 0);
  return {
    count:   list.length,
    average: +(total / list.length).toFixed(2),
    top:     sortBy(list, "marks", true)[0].name,
    pass:    list.filter(s => s.marks >= 50).length
  };
}

5. Execution and Results

OUTPUT

opened 13_arrays.html in Chromium, 900 px wide: "Experiment 13 — Arrays and objects"

as it opens:
  21 | Asha | 72 | DS
  22 | Ravi | 45 | DS
  23 | Meena | 91 | Stats
  24 | Kiran | 66 | DS
  25 | Bhanu | 38 | Stats
  5 students · average 62.4 · top Meena · 3 passed

after adding Divya, roll 106:
  21 | Asha | 72 | DS
  22 | Ravi | 45 | DS
  23 | Meena | 91 | Stats
  24 | Kiran | 66 | DS
  25 | Bhanu | 38 | Stats
  106 | Divya | 88 | Stats
  6 students · average 66.67 · top Meena · 4 passed

adding roll 106 again: 'Roll 106 already exists'

sorted by marks, highest first (the heading clicked twice):
  23 | Meena | 91 | Stats
  106 | Divya | 88 | Stats
  21 | Asha | 72 | DS
  24 | Kiran | 66 | DS
  22 | Ravi | 45 | DS
  25 | Bhanu | 38 | Stats
  6 students · average 66.67 · top Meena · 4 passed
  [screenshot 1: sorted by marks]

searching for 'ra':
  24 | Kiran | 66 | DS
  22 | Ravi | 45 | DS
  2 students · average 55.5 · top Kiran · 1 passed

after deleting roll 106:
  23 | Meena | 91 | Stats
  21 | Asha | 72 | DS
  24 | Kiran | 66 | DS
  22 | Ravi | 45 | DS
  25 | Bhanu | 38 | Stats
  5 students · average 62.4 · top Meena · 3 passed
  [screenshot 2: after the delete]

13_arrays.html: screenshot 1 of 2, taken while the program ran

13_arrays.html: screenshot 2 of 2, taken while the program ran

Asserted by the runner against a five-student fixture: the average is 62.4, the top scorer is Meena, adding a duplicate roll throws, and summary([]) returns nulls rather than NaN. The page shows the same.

RESULT

The five students average 62.4, with Meena top; a sixth is added, a duplicate roll is refused, the table sorts by marks and filters by name, and a delete restores the five.

Experiment 14 — Render JSON as a table

1. Question

Fetch student information held in a JSON file, and display it as a table.

2. Aim

Load JSON with fetch(), and draw it into a table safely and efficiently.

3. Steps

In HTML, 14_json_table.html:

  1. Set up the head, and link the stylesheet.
  2. A search box, a status line and the table.
  3. Draw the rows that match, in order.
  4. Filter, and sort by a column.

In JavaScript, 14_json_table.js:

  1. Turn the JSON into rows.
  2. Sort and filter the rows.
  3. Summarise them.
  4. Draw them, as text.
  5. Load the file, checking the response.

THREE DELIBERATE CHOICES

textContent, not innerHTML. The data came from a file or an API, so a name of <img src=x onerror=alert(1)> must render as text. This is the XSS rule from Unit 4, in the one place students most often break it.

A DocumentFragment. Rows are built off-document and inserted once, so the browser reflows once rather than once per row.

Optional chaining on s.marks?.maths. One student in the fixture has no marks object at all; without the ?. the whole render throws and the page goes blank instead of showing a dash.

4. Programme

In HTML, 14_json_table.html:

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head, and link the stylesheet -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 14 — Display JSON data in a table</title>
<link rel="stylesheet" href="07_styled_form.css">
<style>
  table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
  th, td { border: 1px solid #d6dbe4; padding: 8px 10px; text-align: left; }
  thead th { background: #eef2f7; cursor: pointer; user-select: none; }
  tbody tr:nth-child(even) { background: #fafbfd; }
  td.missing { color: #999; text-align: center; }
  #summary { background: #eef2f7; border-radius: 6px; padding: .8rem 1rem;
             margin-top: 1rem; }
  #status { color: #c92a2a; }
</style>
</head>
<body>

<h1>Student data from JSON</h1>

<!-- Step 2: A search box, a status line and the table -->
<form onsubmit="return false">
  <fieldset>
    <legend>Filter</legend>
    <label for="q">Search any column</label>
    <input type="search" id="q" name="q" placeholder="Type to filter">
  </fieldset>
</form>

<p id="status" role="status" aria-live="polite">Loading…</p>

<table>
  <thead>
    <tr>
      <th data-key="roll"  scope="col">Roll</th>
      <th data-key="name"  scope="col">Name</th>
      <th data-key="dept"  scope="col">Department</th>
      <th data-key="maths" scope="col">Maths</th>
      <th data-key="stats" scope="col">Statistics</th>
      <th data-key="total" scope="col">Total</th>
    </tr>
  </thead>
  <tbody id="body"></tbody>
</table>

<div id="summary"></div>

<p><small>Bhanu Prakash has no <code>marks</code> object in
  <code>students.json</code>. Optional chaining
  (<code>s.marks?.maths ?? null</code>) is why the page shows a dash for that
  row instead of going blank with a <code>TypeError</code>.</small></p>

<script type="module">
  import { loadStudents, renderTable, sortRows, filterRows, summarise }
    from "./14_json_table.js";

  const body = document.getElementById("body");
  const status = document.getElementById("status");
  let rows = [], key = "roll", desc = false;

  // Step 3: Draw the rows that match, in order
  function draw() {
    const shown = sortRows(filterRows(rows, document.getElementById("q").value),
                           key, desc);
    renderTable(shown, body);
    const s = summarise(shown);
    document.getElementById("summary").textContent = s.avgTotal === null
      ? `${s.count} rows, none with marks`
      : `${s.count} rows · ${s.scored} with marks · average total ${s.avgTotal} · top ${s.top}`;
  }

  // Step 4: Filter, and sort by a column
  document.getElementById("q").addEventListener("input", draw);

  document.querySelectorAll("thead th[data-key]").forEach(th =>
    th.addEventListener("click", () => {
      desc = th.dataset.key === key ? !desc : false;
      key = th.dataset.key;
      draw();
    }));

  try {
    rows = await loadStudents("students.json");
    status.textContent = "";
    draw();
  } catch (e) {
    // Opening this file with file:// blocks fetch under the CORS policy.
    // Serve it: python3 -m http.server, then visit localhost:8000.
    status.textContent = `Could not load data: ${e.message}. ` +
      `Serve the folder over http:// rather than opening the file directly.`;
  }
</script>

</body>
</html>

In JavaScript, 14_json_table.js:

/* Experiment 14 — Fetch and display student information held in JSON.
 *
 * Three deliberate choices in renderTable, each worth a mark in the viva:
 *
 *   1. textContent, NOT innerHTML. The data came from a file or an API, so a
 *      name of "<img src=x onerror=alert(1)>" must render as text. This is
 *      the XSS rule from Unit 4, in the one place students most often break it.
 *
 *   2. A DocumentFragment. Rows are built off-document and inserted once, so
 *      the browser reflows once instead of once per row.
 *
 *   3. Optional chaining on s.marks?.maths. One student in students.json has
 *      no marks object at all; without the ?. the whole render throws and the
 *      page goes blank instead of showing a dash.
 */

// Step 1: Turn the JSON into rows
export const COLUMNS = ["roll", "name", "dept", "maths", "stats", "total"];

export function toRows(data) {
  return data.students.map(s => ({
    roll:  s.roll,
    name:  s.name,
    dept:  s.dept,
    maths: s.marks?.maths ?? null,
    stats: s.marks?.stats ?? null,
    total: (s.marks?.maths ?? 0) + (s.marks?.stats ?? 0)
  }));
}

// Step 2: Sort and filter the rows
export function sortRows(rows, key, desc = false) {
  return [...rows].sort((a, b) => {
    const [x, y] = desc ? [b[key], a[key]] : [a[key], b[key]];
    if (x === null) return 1;              // nulls always sink to the bottom
    if (y === null) return -1;
    return typeof x === "string" ? x.localeCompare(y) : x - y;
  });
}

export function filterRows(rows, term) {
  const t = String(term).trim().toLowerCase();
  if (t === "") return rows;
  return rows.filter(r =>
    Object.values(r).some(v => String(v ?? "").toLowerCase().includes(t)));
}

// Step 3: Summarise them
export function summarise(rows) {
  const scored = rows.filter(r => r.maths !== null && r.stats !== null);
  if (scored.length === 0) return { count: rows.length, avgTotal: null, top: null };
  const sum = scored.reduce((a, r) => a + r.total, 0);
  return {
    count:    rows.length,
    scored:   scored.length,
    avgTotal: +(sum / scored.length).toFixed(2),
    top:      sortRows(scored, "total", true)[0].name
  };
}

// Step 4: Draw them, as text
export function renderTable(rows, tbody, doc = document) {
  tbody.textContent = "";
  const frag = doc.createDocumentFragment();

  for (const r of rows) {
    const tr = doc.createElement("tr");
    for (const col of COLUMNS) {
      const td = doc.createElement("td");
      td.textContent = r[col] ?? "—";
      if (r[col] === null) td.className = "missing";
      tr.append(td);
    }
    frag.append(tr);
  }

  tbody.append(frag);                       // ONE insertion into the document
  return tbody;
}

// Step 5: Load the file, checking the response
/** Load and parse, with the two guards that matter.
 *  fetchFn is injectable so the runner can test the failure path offline. */
export async function loadStudents(url = "students.json", fetchFn = fetch) {
  const res = await fetchFn(url);
  // fetch does NOT reject on 404 — an error status is still a successful HTTP
  // transaction. Skip this check and you call .json() on an HTML error page.
  if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
  return toRows(await res.json());
}

5. Execution and Results

OUTPUT

opened 14_json_table.html in Chromium, 900 px wide: "Experiment 14 — Display JSON data in a table"

loaded from students.json:   status ''
  21 | Asha Kumari | Data Science | 88 | 91 | 179
  22 | Ravi Teja | Data Science | 65 | 58 | 123
  23 | Meena Devi | Statistics | 94 | 89 | 183
  24 | Kiran Kumar | Data Science | 71 | 66 | 137
  25 | Bhanu Prakash | Statistics | — | — | 0
  5 rows · 4 with marks · average total 155.5 · top Meena Devi
  2 cell(s) shown as a dash: a student with no marks
  [screenshot 1: as loaded]

sorted by total, highest first:   status ''
  23 | Meena Devi | Statistics | 94 | 89 | 183
  21 | Asha Kumari | Data Science | 88 | 91 | 179
  24 | Kiran Kumar | Data Science | 71 | 66 | 137
  22 | Ravi Teja | Data Science | 65 | 58 | 123
  25 | Bhanu Prakash | Statistics | — | — | 0
  5 rows · 4 with marks · average total 155.5 · top Meena Devi

filtered for 'stat':   status ''
  23 | Meena Devi | Statistics | 94 | 89 | 183
  25 | Bhanu Prakash | Statistics | — | — | 0
  2 rows · 1 with marks · average total 183 · top Meena Devi
  [screenshot 2: filtered]

14_json_table.html: screenshot 1 of 2, taken while the program ran

14_json_table.html: screenshot 2 of 2, taken while the program ran

The doc parameter defaults to document but can be passed jsdom's document, which is how the runner tests this without a browser. Bhanu Prakash, the student with no marks, shows two dashes and a total of 0, and is left out of the average.

RESULT

The five students load from students.json, with an average total of 155.5 and Meena Devi top; the table sorts by total and filters to the Statistics students.

Experiment 15 — Fetch weather data from an open API

1. Question

Fetch real-time weather data from an open API, and display it.

2. Aim

Call a web API with fetch(), check the response, and show its data.

3. Steps

In HTML, 15_weather.html:

  1. Set up the head, and link the stylesheet.
  2. A form for the city and the API key.
  3. A card for the weather.
  4. Show a summary on the card.
  5. Fetch the weather on submit.
  6. Or show the saved sample.

In JavaScript, 15_weather.js:

  1. Pick the fields to show from the response.
  2. Convert to Fahrenheit.
  3. Build the request, and fetch it.

THE POINT OF THE EXPERIMENT

The runner has no network access and no API key, so it tests summarise against a saved sample response, and tests fetchWeather with a stub fetchFn that returns { ok: false, status: 404 } — asserting that it throws rather than returning undefined.

That split is the point of the experiment. Separating the network call from the parsing makes the parsing testable, and the parsing is where the bugs actually are.

4. Programme

In HTML, 15_weather.html:

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head, and link the stylesheet -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 15 — Weather from an open API</title>
<link rel="stylesheet" href="07_styled_form.css">
<style>
  .wx { max-width: 560px; margin: 1rem auto; background: #fff; padding: 1.5rem;
        border: 1px solid #ccd3dd; border-radius: 10px; }
  .wx h2 { margin: 0 0 .2rem; color: #2b4c7e; }
  .temp { font-size: 3rem; font-weight: 700; margin: .2rem 0; }
  .cond { text-transform: capitalize; color: #5b6675; }
  dl { display: grid; grid-template-columns: auto 1fr; gap: .3rem 1rem;
       margin-top: 1rem; }
  dt { font-weight: 600; } dd { margin: 0; }
  .row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: end; }
  .row > div { flex: 1 1 160px; }
</style>
</head>
<body>

<h1>Weather</h1>

<!-- Step 2: A form for the city and the API key -->
<form id="wxform">
  <fieldset>
    <legend>Look up a city</legend>
    <div class="row">
      <div><label for="city">City</label>
           <input type="text" id="city" name="city" value="Vijayawada" required></div>
      <div><label for="key">API key</label>
           <input type="text" id="key" name="key" placeholder="openweathermap.org key"></div>
      <div><button type="submit">Fetch</button>
           <button type="button" id="demo">Use saved sample</button></div>
    </div>
    <p id="status" class="error" role="status" aria-live="polite"></p>
  </fieldset>
</form>

<!-- Step 3: A card for the weather -->
<div class="wx" id="out" hidden>
  <h2 id="place"></h2>
  <p class="temp" id="temp"></p>
  <p class="cond" id="cond"></p>
  <dl>
    <dt>Feels like</dt><dd id="feels"></dd>
    <dt>Humidity</dt><dd id="hum"></dd>
    <dt>Pressure</dt><dd id="press"></dd>
    <dt>Wind</dt><dd id="wind"></dd>
    <dt>Cloud cover</dt><dd id="cloud"></dd>
  </dl>
</div>

<p><small>No key? Press <strong>Use saved sample</strong>. It reads
  <code>weather-sample.json</code> — a real OpenWeatherMap response saved to
  disk — through exactly the same <code>summarise()</code> function, which is
  also how the automated test runs offline.</small></p>

<script type="module">
  import { fetchWeather, summarise, toF } from "./15_weather.js";

  const status = document.getElementById("status");
  const out = document.getElementById("out");

  // Step 4: Show a summary on the card
  function show(w) {
    document.getElementById("place").textContent =
      w.country ? `${w.place}, ${w.country}` : w.place;
    document.getElementById("temp").textContent  = `${w.tempC} °C`;
    document.getElementById("cond").textContent  = w.condition;
    document.getElementById("feels").textContent = `${w.feelsC} °C (${toF(w.feelsC)} °F)`;
    document.getElementById("hum").textContent   = `${w.humidity} %`;
    document.getElementById("press").textContent = `${w.pressure} hPa`;
    document.getElementById("wind").textContent  =
      w.windMs === null ? "—" : `${w.windMs} m/s`;
    document.getElementById("cloud").textContent =
      w.cloudPct === null ? "—" : `${w.cloudPct} %`;
    out.hidden = false;
  }

  // Step 5: Fetch the weather on submit
  document.getElementById("wxform").addEventListener("submit", async e => {
    e.preventDefault();
    status.textContent = "Fetching…";
    out.hidden = true;
    try {
      show(await fetchWeather(document.getElementById("city").value.trim(),
                              document.getElementById("key").value.trim()));
      status.textContent = "";
    } catch (err) {
      status.textContent = err.message;
    }
  });

  // Step 6: Or show the saved sample
  document.getElementById("demo").addEventListener("click", async () => {
    status.textContent = "";
    try {
      const res = await fetch("weather-sample.json");
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      show(summarise(await res.json()));
    } catch (err) {
      status.textContent = `${err.message} — serve this folder over http:// ` +
        `rather than opening the file directly.`;
    }
  });
</script>

</body>
</html>

In JavaScript, 15_weather.js:

/* Experiment 15 — Fetch real-time weather from an open API and display it.
 *
 * The module is split in two on purpose:
 *
 *   summarise()    — pure. Takes a parsed response, returns display values.
 *                    Testable offline against weather-sample.json, which is a
 *                    real OpenWeatherMap response saved to disk.
 *   fetchWeather() — does the network call, then delegates to summarise().
 *                    fetchFn is injectable so the failure path can be tested
 *                    without a network or an API key.
 *
 * Separating the network call from the parsing is what makes the parsing
 * testable, and the parsing is where the bugs actually are.
 *
 * THREE THINGS TO SAY IN THE VIVA
 *
 *   1. fetch does NOT reject on a 404. An error status is still a successful
 *      HTTP transaction, so res.ok must be checked explicitly. Skip it and you
 *      call .json() on an HTML error page and get a confusing SyntaxError.
 *
 *   2. An API key in front-end JavaScript is visible to anyone who opens dev
 *      tools. Fine for a lab with a free key; in production the request goes
 *      through a server that holds the key.
 *
 *   3. CORS may block the request entirely. It is a BROWSER restriction — the
 *      same request from curl succeeds — and it cannot be worked around from
 *      the client. An API that forbids browser access needs a server proxy.
 */

const ENDPOINT = "https://api.openweathermap.org/data/2.5/weather";

// Step 1: Pick the fields to show from the response
export function summarise(json) {
  return {
    place:     json.name,
    country:   json.sys?.country ?? "",
    tempC:     Math.round(json.main.temp),
    feelsC:    Math.round(json.main.feels_like),
    humidity:  json.main.humidity,
    pressure:  json.main.pressure,
    condition: json.weather?.[0]?.description ?? "unknown",
    icon:      json.weather?.[0]?.icon ?? null,
    windMs:    json.wind?.speed ?? null,
    cloudPct:  json.clouds?.all ?? null
  };
}

// Step 2: Convert to Fahrenheit
/** Celsius to Fahrenheit, because the viva always asks. */
export const toF = c => +(c * 9 / 5 + 32).toFixed(1);

// Step 3: Build the request, and fetch it
export function buildUrl(city, key, units = "metric") {
  const url = new URL(ENDPOINT);
  // URLSearchParams escapes the city name correctly — "New Delhi" becomes
  // "New+Delhi" without any manual encodeURIComponent.
  url.search = new URLSearchParams({ q: city, appid: key, units });
  return url;
}

export async function fetchWeather(city, key, fetchFn = fetch) {
  if (!key) throw new Error("An API key is required. Get a free one at openweathermap.org.");
  const res = await fetchFn(buildUrl(city, key));
  if (!res.ok) {
    if (res.status === 401) throw new Error("Invalid API key");
    if (res.status === 404) throw new Error(`City "${city}" not found`);
    throw new Error(`HTTP ${res.status} ${res.statusText}`);
  }
  return summarise(await res.json());
}

5. Execution and Results

OUTPUT

opened 15_weather.html in Chromium, 900 px wide: "Experiment 15 — Weather from an open API"
the sample response, through summarise():
  place  Vijayawada, IN
  temp   30 °C
  cond   Moderate Rain
  feels  34 °C (93.2 °F)
  hum    78 %
  press  1004 hPa
  wind   4.63 m/s
  cloud  90 %
  [screenshot 1: the saved sample]
  console.error: Failed to load resource: net::ERR_INTERNET_DISCONNECTED

a real fetch, which cannot reach the API from here: status 'Failed to fetch'
  [screenshot 2: a fetch that failed]

15_weather.html: screenshot 1 of 2, taken while the program ran

15_weather.html: screenshot 2 of 2, taken while the program ran

The page has the same split: Use saved sample puts the saved response through the same summarise(). The driver presses it, then tries a real fetch, which it stops, as the API cannot be reached from here; the page says so in its status line rather than going blank.

Three things to say in the viva.

fetch does not reject on a 404 — an error status is still a successful HTTP transaction, so res.ok must be checked explicitly. Skip it and you call .json() on an HTML error page and get a confusing SyntaxError.

An API key in front-end JavaScript is visible to anyone who opens dev tools. Acceptable for a lab with a free key; in production the request goes through a server that holds the key.

CORS may block the request entirely. It is a browser restriction — the same request from curl succeeds — and it cannot be worked around from the client. If an API does not permit browser access, it needs a server-side proxy.

RESULT

The sample shows Vijayawada at 30 °C, feeling like 34 °C (93.2 °F), in moderate rain; a fetch that cannot reach the API is reported as "Failed to fetch".

Experiment 16 — jQuery DOM manipulation

1. Question

Use jQuery to hide, show, fade, slide and toggle elements.

2. Aim

Change a page with jQuery's effects, chaining and event delegation.

3. Steps

In HTML, 16_jquery.html:

  1. Set up the head, and link the stylesheet.
  2. A panel to hide, show, fade, slide and toggle.
  3. A box to animate.
  4. A message to change by chaining.
  5. A table whose rows can be added and deleted.
  6. Load jQuery.
  7. Wire every button with jQuery.
  8. Or, where jQuery did not load, with plain JavaScript.

In CSS, 16_jquery.css:

  1. Style the page and the buttons.
  2. The panel's states, for the library-free version.
  3. The box, the table, and the message.

In JavaScript, 16_jquery_native.js:

  1. Hide, show and toggle.
  2. Fade and slide.
  3. Set a message.
  4. Delete rows by delegation.
  5. Move, append and empty.

FOUR EXAM POINTS

Delegation. $("#table").on("click", ".delete-btn", …) binds one listener to the table, so rows added by AJAX afterwards work. $(".delete-btn").on(…) binds only to rows that exist at that instant.

.closest("tr"), not .parent().parent(). Wrap the button in a <span> for styling and the parent chain now removes the wrong element.

this inside a jQuery handler is a raw DOM element — wrap it as $(this) to use jQuery methods on it. And an arrow function handler gets the enclosing scope's this, so $(this) will not be the element; use function handlers with jQuery.

.stop(true) before an animation clears the queue. Without it, five rapid clicks queue five animations and the box keeps moving long after you stopped clicking.

4. Programme

In HTML, 16_jquery.html:

<!DOCTYPE html>
<html lang="en">
<!-- Step 1: Set up the head, and link the stylesheet -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Experiment 16 — jQuery DOM manipulation</title>
<link rel="stylesheet" href="16_jquery.css">
</head>
<body>

<h1>jQuery: hide, show, fade, slide, toggle</h1>

<p><small>This page loads jQuery from a CDN. With no network it falls back to
  <code>16_jquery_native.js</code>, which does exactly the same things with no
  library — compare the two.</small></p>

<!-- Step 2: A panel to hide, show, fade, slide and toggle -->
<h2>Effects</h2>
<p>
  <button type="button" id="hide">hide()</button>
  <button type="button" id="show">show()</button>
  <button type="button" id="fade">fadeToggle()</button>
  <button type="button" id="slide">slideToggle()</button>
  <button type="button" id="toggle">toggle()</button>
</p>

<div id="panel">
  <p>Effects queue <strong>per element</strong>: each waits for the previous to
     finish. That is why they chain so readably — and why a hover effect
     without <code>.stop()</code> keeps animating long after the pointer has
     left, because every hover queued another animation.</p>
</div>

<!-- Step 3: A box to animate -->
<h2>animate()</h2>
<p><button type="button" id="animate">animate()</button>
   <button type="button" id="reset">reset</button></p>
<div id="box"></div>

<!-- Step 4: A message to change by chaining -->
<h2>Chaining</h2>
<p><button type="button" id="ok">Success</button>
   <button type="button" id="bad">Error</button></p>
<p id="msg"></p>
<pre><code>$("#msg").removeClass("error").addClass("success")
         .text("Saved successfully").fadeIn(300);</code></pre>
<p><small><strong>Getters break the chain.</strong> <code>.text()</code> with
  no argument returns a string, and a string has no <code>.css()</code>. Only
  setters return the jQuery object.</small></p>

<!-- Step 5: A table whose rows can be added and deleted -->
<h2>Event delegation</h2>
<p><button type="button" id="addrow">Add a row</button></p>
<table id="table">
  <thead><tr><th scope="col">Roll</th><th scope="col">Name</th><th scope="col">Action</th></tr></thead>
  <tbody id="tbody">
    <tr><td>21</td><td>Asha</td><td><button type="button" class="delete-btn">Delete</button></td></tr>
    <tr><td>22</td><td>Ravi</td><td><button type="button" class="delete-btn">Delete</button></td></tr>
    <tr><td>23</td><td>Meena</td><td><button type="button" class="delete-btn">Delete</button></td></tr>
  </tbody>
</table>
<p><small>Add a row, then delete it. It works because one listener sits on the
  <em>table</em>, not on each button — <code>$("#table").on("click",
  ".delete-btn", …)</code>. Binding to <code>$(".delete-btn")</code> directly
  would leave every new row dead.</small></p>

<!-- Step 6: Load jQuery -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
        integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
        crossorigin="anonymous"></script>

<script>
// Step 7: Wire every button with jQuery
if (window.jQuery) {
  $(function () {
    $("#hide").on("click",   function () { $("#panel").hide(400); });
    $("#show").on("click",   function () { $("#panel").show(400); });
    $("#fade").on("click",   function () { $("#panel").fadeToggle(300); });
    $("#slide").on("click",  function () { $("#panel").slideToggle(300); });
    $("#toggle").on("click", function () { $("#panel").toggle(300); });

    $("#animate").on("click", function () {
      // .stop(true) clears the queue: without it five rapid clicks queue five
      // animations and the box keeps moving long after you stopped clicking.
      $("#box").stop(true).animate({ left: "250px", opacity: 0.5 }, 500,
        function () { $(this).addClass("done"); });
    });
    $("#reset").on("click", function () {
      $("#box").stop(true).css({ left: 0, opacity: 1 }).removeClass("done");
    });

    // Chaining. Note `this` inside a jQuery handler is a RAW DOM ELEMENT —
    // $(this) is what wraps it back up. An arrow function here would take
    // `this` from the enclosing scope and break.
    $("#ok").on("click", function () {
      $("#msg").removeClass("error").addClass("success")
               .text("Saved successfully").fadeIn(300);
    });
    $("#bad").on("click", function () {
      $("#msg").removeClass("success").addClass("error")
               .text("Could not save").fadeIn(300);
    });

    // Delegation: the selector is the SECOND argument.
    $("#table").on("click", ".delete-btn", function () {
      $(this).closest("tr").fadeOut(300, function () { $(this).remove(); });
    });

    let next = 24;
    $("#addrow").on("click", function () {
      $("#tbody").append(
        '<tr><td>' + (next++) + '</td><td>New student</td>' +
        '<td><button type="button" class="delete-btn">Delete</button></td></tr>');
    });
  });
}
</script>

<script type="module">
// Step 8: Or, where jQuery did not load, with plain JavaScript
if (!window.jQuery) {
  const { hide, show, toggle, fadeToggle, slideToggle, setMessage,
          wireTable, slideTo } = await import("./16_jquery_native.js");

  const panel = document.getElementById("panel");
  const box   = document.getElementById("box");
  const msg   = document.getElementById("msg");

  document.getElementById("hide").addEventListener("click",   () => hide(panel));
  document.getElementById("show").addEventListener("click",   () => show(panel));
  document.getElementById("toggle").addEventListener("click", () => toggle(panel));
  document.getElementById("fade").addEventListener("click",   () => fadeToggle(panel));
  document.getElementById("slide").addEventListener("click",  () => slideToggle(panel));

  document.getElementById("animate").addEventListener("click", () => slideTo(box, 250));
  document.getElementById("reset").addEventListener("click",   () => slideTo(box, 0));

  document.getElementById("ok").addEventListener("click",
    () => setMessage(msg, "Saved successfully", "success"));
  document.getElementById("bad").addEventListener("click",
    () => setMessage(msg, "Could not save", "error"));

  wireTable(document.getElementById("table"));

  let next = 24;
  document.getElementById("addrow").addEventListener("click", () => {
    const tr = document.createElement("tr");
    for (const v of [next++, "New student"]) {
      const td = document.createElement("td");
      td.textContent = String(v);
      tr.append(td);
    }
    const td = document.createElement("td");
    const btn = document.createElement("button");
    btn.type = "button";
    btn.className = "delete-btn";
    btn.textContent = "Delete";
    td.append(btn);
    tr.append(td);
    document.getElementById("tbody").append(tr);
  });
}
</script>

</body>
</html>

In CSS, 16_jquery.css:

/* The visual half of experiment 16. Every effect the jQuery version animates
   in JavaScript is expressed here as a CSS transition, which is what the
   native version toggles. */

/* Step 1: Style the page and the buttons */
*, *::before, *::after { box-sizing: border-box; }

body { font-family: system-ui, sans-serif; line-height: 1.6;
       max-width: 780px; margin: 2rem auto; padding: 0 1rem; color: #1f2733; }

h2 { color: #2b4c7e; margin-top: 2.2rem; }

button { font: inherit; padding: 8px 14px; margin: 0 .3rem .5rem 0;
         border: 0; border-radius: 6px; background: #2b4c7e; color: #fff;
         cursor: pointer; }
button:hover { background: #1f3a63; }

/* Step 2: The panel's states, for the library-free version */
#panel { background: #eef2f7; border: 1px solid #ccd3dd; border-radius: 8px;
         padding: 1rem; overflow: hidden;
         transition: opacity .3s ease, max-height .3s ease, padding .3s ease; }
#panel.is-hidden    { display: none; }
#panel.is-faded     { opacity: 0; }
#panel.is-collapsed { max-height: 0; padding-top: 0; padding-bottom: 0; opacity: 0; }

/* Step 3: The box, the table, and the message */
#box { width: 60px; height: 60px; background: #2b8a3e; border-radius: 8px;
       transition: transform .5s ease, opacity .5s ease; }

table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
th, td { border: 1px solid #d6dbe4; padding: 8px 10px; text-align: left; }
th { background: #eef2f7; }
tr { transition: opacity .3s ease; }
tr.fading { opacity: 0; }

.delete-btn { background: #c92a2a; padding: 3px 9px; font-size: .85rem; }
.delete-btn:hover { background: #a51f1f; }

#msg { padding: .7rem 1rem; border-radius: 6px; min-height: 1.5rem; }
#msg.success { background: #e7f5eb; color: #1c6b2c; }
#msg.error   { background: #fdecec; color: #8c1f1f; }

In JavaScript, 16_jquery_native.js:

/* Experiment 16 — the same behaviour as 16_jquery.html, with no library.
 *
 * jQuery's animation queue depends on timing that jsdom does not reproduce
 * faithfully, so this native version is what tools/run_web_labs.js executes:
 * class toggles can be asserted directly and deterministically.
 *
 * Read it beside the jQuery in 16_jquery.html. Every line has a one-to-one
 * counterpart, which is the real lesson of this experiment — jQuery's ideas
 * succeeded so completely that the library became unnecessary.
 */

// Step 1: Hide, show and toggle
/* jQuery: $(el).hide()  /  $(el).show() */
export const hide = el => { el.classList.add("is-hidden"); return el; };
export const show = el => { el.classList.remove("is-hidden"); return el; };

/* jQuery: $(el).toggle() */
export const toggle = el => { el.classList.toggle("is-hidden"); return el; };

// Step 2: Fade and slide
/* jQuery: $(el).fadeToggle() — the fade lives in the stylesheet, where the
   browser can run it on the compositor instead of a JavaScript timer. */
export const fadeToggle = el => { el.classList.toggle("is-faded"); return el; };

/* jQuery: $(el).slideToggle() */
export const slideToggle = el => { el.classList.toggle("is-collapsed"); return el; };

// Step 3: Set a message
/* jQuery: $(el).addClass(a).removeClass(b).text(t) — chaining works here too,
   because each function returns the element. */
export function setMessage(el, text, kind = "success") {
  el.classList.remove("error", "success");
  el.classList.add(kind);
  el.textContent = text;              // textContent, never innerHTML
  return el;
}

// Step 4: Delete rows by delegation
/* jQuery: $("#table").on("click", ".delete-btn", fn)
 *
 * Event DELEGATION. One listener on the container, so rows added later — by
 * AJAX, or by the user — work with no extra wiring. Binding directly to
 * ".delete-btn" would only catch the rows that existed at that instant.
 *
 * closest("tr") rather than parentNode.parentNode: wrap the button in a
 * <span> for styling and a parent chain removes the wrong element.
 */
export function wireTable(root) {
  root.addEventListener("click", e => {
    const btn = e.target.closest(".delete-btn");
    if (!btn) return;
    const tr = btn.closest("tr");
    if (!tr) return;
    // Remove after the CSS fade finishes — the native equivalent of jQuery's
    // fadeOut(300, function () { $(this).remove(); }) completion callback.
    tr.addEventListener("transitionend", () => tr.remove(), { once: true });
    tr.classList.add("fading");
  });
  return root;
}

// Step 5: Move, append and empty
/* jQuery: $(el).animate({ left: "250px" }, 500) */
export function slideTo(el, x) {
  el.style.transform = `translateX(${x}px)`;
  return el;
}

/* jQuery: $("#list").append("<li>…</li>") */
export function appendItem(list, text, doc = document) {
  const li = doc.createElement("li");
  li.textContent = text;
  list.append(li);
  return li;
}

/* jQuery: $("#list").empty() */
export const empty = el => { el.textContent = ""; return el; };

5. Execution and Results

OUTPUT

opened 16_jquery.html in Chromium, 900 px wide: "Experiment 16 — jQuery DOM manipulation"
jQuery loaded: 3.7.1  (so the jQuery version of the page is the one running)
  hide    -> the panel is hidden
  show    -> the panel is shown, opacity 1
  fade    -> the panel is hidden
  fade    -> the panel is shown, opacity 1
  slide   -> the panel is hidden
  slide   -> the panel is shown, opacity 1
  toggle  -> the panel is hidden
  toggle  -> the panel is shown, opacity 1
  animate -> the box is at left 250px, class 'done'
  [screenshot 1: after animate()]
  ok      -> the message reads 'Saved successfully', class 'success'
  rows: 3, 4 after Add row, 3 after deleting the new one -- delegation works for a row added later
  [screenshot 2: after the rows]

16_jquery.html: screenshot 1 of 2, taken while the program ran

16_jquery.html: screenshot 2 of 2, taken while the program ran

16_jquery_native.js is the same behaviour with no library, and it is what the runner executes under jsdom — jQuery's animation queue depends on timing that jsdom does not reproduce faithfully, while the native version's class toggles can be asserted directly. Asserted: clicking a .delete-btn adds fading to the correct <tr>, and a click elsewhere in the table does nothing.

In Chromium, jQuery loads, so the jQuery version is the one that runs, and its animations play out in real time; the clock is not fixed for this page, as jQuery times its animations by it.

RESULT

jQuery 3.7.1 loads; each effect hides or shows the panel, animate() moves the box to 250 px, the chained message reads "Saved successfully", and a row added after the page loaded can be deleted.


Lab examination

The lab exam gives you one experiment, a browser and a text editor, roughly an hour, and then a viva.

What actually costs marks:

What earns them: