Ad Code

✨🎆 JOIN MERN, JAVA, PYTHON, AI, DEVOPS, SALESFORCE Courses 🎆✨

Get 100% Placement Oriented Program CLICK to new more info click

Complete Marksheet APP in Angular 21 Classroom Projects

 Yes. This is an excellent Angular 21 Reactive Forms + FormArray classroom project because it demonstrates dynamic controls, validation, calculated results, conditional UI, and business rules.

Below is a complete application for a Dynamic Student Marksheet.

Business rules implemented

  1. Student enters:
    • Student Name
    • Class Name
    • Roll Number
    • Dynamic Subject Name
    • Dynamic Marks
  2. Marks must be 0–100.
  3. Subject marks can be added/removed dynamically.
  4. Marks > 75 → display Distinction.
  5. Subject marks < 33 → subject is failed.
  6. Exactly 1 failed subject → SUPPLEMENTARY.
  7. More than 1 failed subject → FAIL.
  8. If exactly 1 subject is failed:
    • Apply maximum 5 grace marks.
    • If failed subject can reach 33 → PASS BY GRACE.
    • Otherwise → SUPPLEMENTARY.
  9. If no subject is below 33 → PASS.
  10. Percentage and overall grade are calculated dynamically.
  11. Marksheet is displayed only when the form is valid.

1. Create Angular 21 Project

ng new student-marksheet

Select:

Would you like to add Angular routing? No
Which stylesheet format? CSS

Then:

cd student-marksheet
ng serve

The important concept here is:

FormGroup
   |
   +-- studentName
   +-- className
   +-- rollNumber
   |
   +-- subjects : FormArray
                    |
                    +-- FormGroup
                    |      +-- subjectName
                    |      +-- marks
                    |
                    +-- FormGroup
                           +-- subjectName
                           +-- marks

2. app.component.ts

Replace the complete file with:

import { Component } from '@angular/core';
import {
  FormArray,
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  Validators
} from '@angular/forms';

interface SubjectResult {
  subjectName: string;
  marks: number;
  graceMarks: number;
  finalMarks: number;
  distinction: boolean;
  passed: boolean;
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {

  showMarksheet = false;

  marksheetForm = new FormGroup({
    studentName: new FormControl('', [
      Validators.required,
      Validators.minLength(3)
    ]),

    className: new FormControl('', [
      Validators.required
    ]),

    rollNumber: new FormControl('', [
      Validators.required
    ]),

    subjects: new FormArray([
      this.createSubject()
    ])
  });

  // ----------------------------------------------------
  // Create Dynamic Subject
  // ----------------------------------------------------

  createSubject(): FormGroup {
    return new FormGroup({
      subjectName: new FormControl('', [
        Validators.required,
        Validators.minLength(2)
      ]),

      marks: new FormControl<number | null>(null, [
        Validators.required,
        Validators.min(0),
        Validators.max(100)
      ])
    });
  }

  // ----------------------------------------------------
  // Get Subjects FormArray
  // ----------------------------------------------------

  get subjects(): FormArray {
    return this.marksheetForm.get('subjects') as FormArray;
  }

  // ----------------------------------------------------
  // Add Subject
  // ----------------------------------------------------

  addSubject(): void {
    this.subjects.push(this.createSubject());
  }

  // ----------------------------------------------------
  // Remove Subject
  // ----------------------------------------------------

  removeSubject(index: number): void {

    if (this.subjects.length === 1) {
      return;
    }

    this.subjects.removeAt(index);
  }

  // ----------------------------------------------------
  // Get Subject Control
  // ----------------------------------------------------

  getSubject(index: number): FormGroup {
    return this.subjects.at(index) as FormGroup;
  }

  // ----------------------------------------------------
  // Validation Helper
  // ----------------------------------------------------

  isInvalid(
    controlName: string,
    index?: number
  ): boolean {

    if (index !== undefined) {

      const control = this.getSubject(index).get(controlName);

      return !!control &&
        control.invalid &&
        (control.dirty || control.touched);
    }

    const control = this.marksheetForm.get(controlName);

    return !!control &&
      control.invalid &&
      (control.dirty || control.touched);
  }

  // ----------------------------------------------------
  // Generate Marksheet
  // ----------------------------------------------------

  generateMarksheet(): void {

    this.showMarksheet = false;

    if (this.marksheetForm.invalid) {

      this.marksheetForm.markAllAsTouched();

      return;
    }

    this.showMarksheet = true;
  }

  // ----------------------------------------------------
  // Calculate Total Marks
  // ----------------------------------------------------

  getTotalMarks(): number {

    return this.subjects.controls.reduce((total, subject) => {

      const marks = Number(subject.get('marks')?.value ?? 0);

      return total + marks;

    }, 0);
  }

  // ----------------------------------------------------
  // Maximum Marks
  // ----------------------------------------------------

  getMaximumMarks(): number {

    return this.subjects.length * 100;
  }

  // ----------------------------------------------------
  // Percentage
  // ----------------------------------------------------

  getPercentage(): number {

    const maximum = this.getMaximumMarks();

    if (maximum === 0) {
      return 0;
    }

    return (this.getTotalMarks() / maximum) * 100;
  }

  // ----------------------------------------------------
  // Failed Subjects
  // ----------------------------------------------------

  getFailedSubjectCount(): number {

    return this.subjects.controls.filter(subject => {

      const marks = Number(subject.get('marks')?.value ?? 0);

      return marks < 33;

    }).length;
  }

  // ----------------------------------------------------
  // Is Student Passed
  // ----------------------------------------------------

  isPassed(): boolean {

    return this.getFailedSubjectCount() === 0;
  }

  // ----------------------------------------------------
  // Is Supplementary
  // ----------------------------------------------------

  isSupplementary(): boolean {

    const failedSubjects = this.getFailedSubjectCount();

    return failedSubjects === 1 &&
      !this.canPassByGrace();
  }

  // ----------------------------------------------------
  // Grace Marks Calculation
  // ----------------------------------------------------

  getGraceMarks(index: number): number {

    const marks = Number(
      this.getSubject(index).get('marks')?.value ?? 0
    );

    /*
      Maximum 5 grace marks.

      Example:

      Marks = 30
      Required = 33

      Grace = 3
    */

    if (marks >= 33) {
      return 0;
    }

    const requiredMarks = 33 - marks;

    if (requiredMarks <= 5) {
      return requiredMarks;
    }

    return 0;
  }

  // ----------------------------------------------------
  // Check Grace Eligibility
  // ----------------------------------------------------

  canPassByGrace(): boolean {

    const failedSubjects = this.getFailedSubjectCount();

    if (failedSubjects !== 1) {
      return false;
    }

    const failedIndex = this.subjects.controls.findIndex(subject => {

      const marks = Number(
        subject.get('marks')?.value ?? 0
      );

      return marks < 33;

    });

    if (failedIndex === -1) {
      return false;
    }

    const marks = Number(
      this.getSubject(failedIndex).get('marks')?.value ?? 0
    );

    return marks >= 28;
  }

  // ----------------------------------------------------
  // Final Result
  // ----------------------------------------------------

  getResult(): string {

    const failedSubjects = this.getFailedSubjectCount();

    if (failedSubjects === 0) {
      return 'PASS';
    }

    if (failedSubjects === 1) {

      if (this.canPassByGrace()) {
        return 'PASS BY GRACE';
      }

      return 'SUPPLEMENTARY';
    }

    return 'FAIL';
  }

  // ----------------------------------------------------
  // Result CSS Class
  // ----------------------------------------------------

  getResultClass(): string {

    const result = this.getResult();

    switch (result) {

      case 'PASS':
        return 'result-pass';

      case 'PASS BY GRACE':
        return 'result-grace';

      case 'SUPPLEMENTARY':
        return 'result-supplementary';

      default:
        return 'result-fail';
    }
  }

  // ----------------------------------------------------
  // Subject Final Marks
  // ----------------------------------------------------

  getFinalMarks(index: number): number {

    const subject = this.getSubject(index);

    const marks = Number(
      subject.get('marks')?.value ?? 0
    );

    return marks + this.getGraceMarks(index);
  }

  // ----------------------------------------------------
  // Check Distinction
  // ----------------------------------------------------

  isDistinction(index: number): boolean {

    const marks = Number(
      this.getSubject(index).get('marks')?.value ?? 0
    );

    return marks > 75;
  }

  // ----------------------------------------------------
  // Subject Status
  // ----------------------------------------------------

  getSubjectStatus(index: number): string {

    const marks = Number(
      this.getSubject(index).get('marks')?.value ?? 0
    );

    if (marks > 75) {
      return 'DISTINCTION';
    }

    if (marks >= 33) {
      return 'PASS';
    }

    if (this.getGraceMarks(index) > 0) {
      return 'GRACE';
    }

    return 'FAIL';
  }

  // ----------------------------------------------------
  // Overall Grade
  // ----------------------------------------------------

  getGrade(): string {

    const percentage = this.getPercentage();

    if (this.getResult() === 'FAIL') {
      return 'F';
    }

    if (percentage >= 90) {
      return 'A+';
    }

    if (percentage >= 80) {
      return 'A';
    }

    if (percentage >= 70) {
      return 'B+';
    }

    if (percentage >= 60) {
      return 'B';
    }

    if (percentage >= 50) {
      return 'C';
    }

    if (percentage >= 33) {
      return 'D';
    }

    return 'F';
  }

  // ----------------------------------------------------
  // Reset
  // ----------------------------------------------------

  resetForm(): void {

    this.marksheetForm.reset();

    while (this.subjects.length > 0) {
      this.subjects.removeAt(0);
    }

    this.subjects.push(this.createSubject());

    this.showMarksheet = false;
  }
}

3. app.component.html

Replace the complete HTML:

<div class="page">

  <!-- HEADER -->

  <header class="header">

    <div class="brand">

      <div class="brand-icon">
        🎓
      </div>

      <div>
        <h1>Student Marksheet</h1>
        <p>Angular 21 Reactive Forms</p>
      </div>

    </div>

    <div class="angular-badge">
      Angular 21
    </div>

  </header>


  <main class="container">

    <!-- ========================= -->
    <!-- FORM SECTION -->
    <!-- ========================= -->

    <section class="card">

      <div class="section-heading">

        <div>
          <h2>Student Information</h2>
          <p>Enter student details and subject marks</p>
        </div>

        <span class="step-badge">
          STEP 1
        </span>

      </div>


      <form
        [formGroup]="marksheetForm"
        (ngSubmit)="generateMarksheet()"
      >

        <!-- STUDENT DETAILS -->

        <div class="form-grid">

          <!-- Student Name -->

          <div class="form-group">

            <label>
              Student Name
              <span>*</span>
            </label>

            <input
              type="text"
              formControlName="studentName"
              placeholder="Enter student name"
            />

            @if (isInvalid('studentName')) {

              <small class="error">
                Student name is required and must contain at least 3 characters.
              </small>

            }

          </div>


          <!-- Class -->

          <div class="form-group">

            <label>
              Class Name
              <span>*</span>
            </label>

            <input
              type="text"
              formControlName="className"
              placeholder="Example: BCA 3rd Year"
            />

            @if (isInvalid('className')) {

              <small class="error">
                Class name is required.
              </small>

            }

          </div>


          <!-- Roll Number -->

          <div class="form-group">

            <label>
              Roll Number
              <span>*</span>
            </label>

            <input
              type="text"
              formControlName="rollNumber"
              placeholder="Enter roll number"
            />

            @if (isInvalid('rollNumber')) {

              <small class="error">
                Roll number is required.
              </small>

            }

          </div>

        </div>


        <!-- ========================= -->
        <!-- SUBJECT SECTION -->
        <!-- ========================= -->

        <div class="subjects-header">

          <div>

            <h2>Subject Marks</h2>

            <p>
              Add subjects dynamically and enter marks from 0 to 100.
            </p>

          </div>

          <button
            type="button"
            class="btn btn-add"
            (click)="addSubject()"
          >
            + Add Subject
          </button>

        </div>


        <div formArrayName="subjects">

          @for (
            subject of subjects.controls;
            track $index;
            let i = $index
          ) {

            <div
              class="subject-row"
              [formGroupName]="i"
            >

              <div class="subject-number">
                {{ i + 1 }}
              </div>


              <!-- Subject -->

              <div class="form-group">

                <label>
                  Subject Name
                </label>

                <input
                  type="text"
                  formControlName="subjectName"
                  placeholder="Example: Mathematics"
                />

                @if (isInvalid('subjectName', i)) {

                  <small class="error">
                    Subject name is required.
                  </small>

                }

              </div>


              <!-- Marks -->

              <div class="form-group">

                <label>
                  Marks
                </label>

                <input
                  type="number"
                  formControlName="marks"
                  min="0"
                  max="100"
                  placeholder="0 - 100"
                />

                @if (isInvalid('marks', i)) {

                  <small class="error">

                    @if (getSubject(i).get('marks')?.hasError('required')) {
                      Marks are required.
                    }

                    @if (
                      getSubject(i).get('marks')?.hasError('min') ||
                      getSubject(i).get('marks')?.hasError('max')
                    ) {
                      Marks must be between 0 and 100.
                    }

                  </small>

                }

              </div>


              <!-- Live Status -->

              <div class="subject-status">

                @if (
                  getSubject(i).get('marks')?.valid &&
                  getSubject(i).get('marks')?.value !== null
                ) {

                  @if (isDistinction(i)) {

                    <span class="status distinction">
                      🏆 Distinction
                    </span>

                  }
                  @else if (
                    getSubject(i).get('marks')?.value >= 33
                  ) {

                    <span class="status passed">
                      ✓ Pass
                    </span>

                  }
                  @else if (getGraceMarks(i) > 0) {

                    <span class="status grace">
                      +{{ getGraceMarks(i) }} Grace
                    </span>

                  }
                  @else {

                    <span class="status failed">
                      ✕ Fail
                    </span>

                  }

                }

              </div>


              <!-- Delete -->

              <button
                type="button"
                class="delete-btn"
                [disabled]="subjects.length === 1"
                (click)="removeSubject(i)"
                title="Remove subject"
              >
                🗑
              </button>

            </div>

          }

        </div>


        <!-- RULE INFORMATION -->

        <div class="rules-box">

          <h3>Marksheet Rules</h3>

          <div class="rules-grid">

            <div>
              <strong>33+</strong>
              <span>Subject Pass</span>
            </div>

            <div>
              <strong>76+</strong>
              <span>Distinction</span>
            </div>

            <div>
              <strong>1 Failed</strong>
              <span>Supplementary</span>
            </div>

            <div>
              <strong>5 Marks</strong>
              <span>Maximum Grace</span>
            </div>

            <div>
              <strong>2+ Failed</strong>
              <span>Fail</span>
            </div>

          </div>

        </div>


        <!-- BUTTONS -->

        <div class="form-actions">

          <button
            type="button"
            class="btn btn-secondary"
            (click)="resetForm()"
          >
            Reset
          </button>

          <button
            type="submit"
            class="btn btn-primary"
          >
            Generate Marksheet →
          </button>

        </div>

      </form>

    </section>


    <!-- ========================= -->
    <!-- MARKSHEET -->
    <!-- ========================= -->

    @if (showMarksheet) {

      <section class="marksheet card">

        <!-- MARKSHEET HEADER -->

        <div class="marksheet-header">

          <div>

            <span class="certificate-label">
              ACADEMIC RESULT
            </span>

            <h2>Student Marksheet</h2>

            <p>
              Official academic performance summary
            </p>

          </div>

          <div
            class="result-badge"
            [class]="getResultClass()"
          >
            {{ getResult() }}
          </div>

        </div>


        <!-- STUDENT INFO -->

        <div class="student-summary">

          <div>
            <span>Student Name</span>
            <strong>
              {{ marksheetForm.get('studentName')?.value }}
            </strong>
          </div>

          <div>
            <span>Class</span>
            <strong>
              {{ marksheetForm.get('className')?.value }}
            </strong>
          </div>

          <div>
            <span>Roll Number</span>
            <strong>
              {{ marksheetForm.get('rollNumber')?.value }}
            </strong>
          </div>

        </div>


        <!-- RESULT TABLE -->

        <div class="table-container">

          <table>

            <thead>

              <tr>

                <th>#</th>

                <th>Subject</th>

                <th>Original Marks</th>

                <th>Grace</th>

                <th>Final Marks</th>

                <th>Status</th>

              </tr>

            </thead>

            <tbody>

              @for (
                subject of subjects.controls;
                track $index;
                let i = $index
              ) {

                <tr>

                  <td>
                    {{ i + 1 }}
                  </td>

                  <td class="subject-name">

                    {{ subject.get('subjectName')?.value }}

                  </td>

                  <td>

                    {{ subject.get('marks')?.value }}

                    <span class="out-of">
                      / 100
                    </span>

                  </td>

                  <td>

                    @if (getGraceMarks(i) > 0) {

                      <span class="grace-mark">
                        +{{ getGraceMarks(i) }}
                      </span>

                    }
                    @else {

                      <span class="no-grace">
                        —
                      </span>

                    }

                  </td>

                  <td>

                    <strong>
                      {{ getFinalMarks(i) }}
                    </strong>

                  </td>

                  <td>

                    @if (isDistinction(i)) {

                      <span class="table-status distinction">
                        🏆 Distinction
                      </span>

                    }
                    @else if (getSubjectStatus(i) === 'PASS') {

                      <span class="table-status passed">
                        ✓ Pass
                      </span>

                    }
                    @else if (getSubjectStatus(i) === 'GRACE') {

                      <span class="table-status grace">
                        ✓ Grace Pass
                      </span>

                    }
                    @else {

                      <span class="table-status failed">
                        ✕ Fail
                      </span>

                    }

                  </td>

                </tr>

              }

            </tbody>

          </table>

        </div>


        <!-- SUMMARY CARDS -->

        <div class="summary-grid">

          <div class="summary-card">

            <span>Total Marks</span>

            <strong>
              {{ getTotalMarks() }}
              <small>/ {{ getMaximumMarks() }}</small>
            </strong>

          </div>


          <div class="summary-card">

            <span>Percentage</span>

            <strong>
              {{ getPercentage() | number:'1.2-2' }}%
            </strong>

          </div>


          <div class="summary-card">

            <span>Grade</span>

            <strong>
              {{ getGrade() }}
            </strong>

          </div>


          <div class="summary-card">

            <span>Failed Subjects</span>

            <strong>
              {{ getFailedSubjectCount() }}
            </strong>

          </div>

        </div>


        <!-- GRACE INFORMATION -->

        @if (getResult() === 'PASS BY GRACE') {

          <div class="grace-notice">

            <div class="notice-icon">
              ✓
            </div>

            <div>

              <strong>
                Student Passed By Grace
              </strong>

              <p>
                The student had one failed subject and was eligible
                for up to 5 grace marks to reach the minimum passing
                mark of 33.
              </p>

            </div>

          </div>

        }


        <!-- SUPPLEMENTARY INFORMATION -->

        @if (getResult() === 'SUPPLEMENTARY') {

          <div class="supplementary-notice">

            <div class="notice-icon">
              !
            </div>

            <div>

              <strong>
                Supplementary Examination Required
              </strong>

              <p>
                The student has failed in one subject but cannot
                pass using the maximum 5 grace marks.
              </p>

            </div>

          </div>

        }


        <!-- FINAL FOOTER -->

        <div class="marksheet-footer">

          <div>

            <strong>
              Result Generated Successfully
            </strong>

            <p>
              This marksheet was generated using Angular 21
              Reactive Forms.
            </p>

          </div>

          <button
            type="button"
            class="btn btn-secondary"
            (click)="showMarksheet = false"
          >
            ← Edit Marks
          </button>

        </div>

      </section>

    }

  </main>

</div>

4. Important: Number Pipe

Because we are using:

{{ getPercentage() | number:'1.2-2' }}%

Angular standalone components need DecimalPipe.

Modify your import:

import { DecimalPipe } from '@angular/common';

Then:

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    ReactiveFormsModule,
    DecimalPipe
  ],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})

So the top of your TypeScript becomes:

import { Component } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import {
  FormArray,
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  Validators
} from '@angular/forms';

5. app.component.css

Use this complete CSS:

* {
  box-sizing: border-box;
}

:host {
  display: block;
}

body {
  margin: 0;
  font-family:
    Inter,
    "Segoe UI",
    Arial,
    sans-serif;
}

.page {
  min-height: 100vh;
  background:
    radial-gradient(
      circle at top left,
      #e0e7ff 0,
      transparent 35%
    ),
    linear-gradient(
      135deg,
      #f8fafc,
      #eef2ff
    );

  color: #172033;

  padding-bottom: 60px;
}


/* =========================================
   HEADER
========================================= */

.header {
  max-width: 1200px;
  margin: auto;

  padding: 30px 24px;

  display: flex;
  align-items: center;
  justify-content: space-between;
}

.brand {
  display: flex;
  align-items: center;
  gap: 14px;
}

.brand-icon {
  width: 52px;
  height: 52px;

  display: flex;
  align-items: center;
  justify-content: center;

  border-radius: 15px;

  background: linear-gradient(
    135deg,
    #4f46e5,
    #7c3aed
  );

  font-size: 25px;

  box-shadow:
    0 10px 25px rgba(79, 70, 229, 0.25);
}

.brand h1 {
  margin: 0;

  font-size: 24px;
  font-weight: 800;
}

.brand p {
  margin: 4px 0 0;

  color: #64748b;
  font-size: 13px;
}

.angular-badge {
  padding: 9px 15px;

  background: #fff;

  border: 1px solid #e2e8f0;

  border-radius: 50px;

  color: #4f46e5;

  font-weight: 700;

  font-size: 13px;

  box-shadow:
    0 5px 15px rgba(15, 23, 42, 0.05);
}


/* =========================================
   CONTAINER
========================================= */

.container {
  max-width: 1200px;

  margin: auto;

  padding: 0 24px;
}


/* =========================================
   CARD
========================================= */

.card {
  background: rgba(255, 255, 255, 0.92);

  border: 1px solid #e2e8f0;

  border-radius: 24px;

  padding: 30px;

  margin-bottom: 28px;

  box-shadow:
    0 20px 60px rgba(15, 23, 42, 0.08);
}


/* =========================================
   SECTION HEADING
========================================= */

.section-heading,
.subjects-header {
  display: flex;

  justify-content: space-between;

  align-items: center;

  gap: 20px;

  margin-bottom: 25px;
}

.section-heading h2,
.subjects-header h2 {
  margin: 0;

  font-size: 21px;
}

.section-heading p,
.subjects-header p {
  margin: 6px 0 0;

  color: #64748b;

  font-size: 14px;
}

.step-badge {
  padding: 7px 12px;

  border-radius: 50px;

  background: #eef2ff;

  color: #4f46e5;

  font-size: 11px;

  font-weight: 800;

  letter-spacing: 1px;
}


/* =========================================
   FORM
========================================= */

.form-grid {
  display: grid;

  grid-template-columns:
    repeat(3, 1fr);

  gap: 20px;

  margin-bottom: 35px;
}

.form-group {
  display: flex;

  flex-direction: column;

  gap: 7px;
}

.form-group label {
  font-size: 13px;

  font-weight: 700;

  color: #334155;
}

.form-group label span {
  color: #ef4444;
}

.form-group input {
  width: 100%;

  height: 46px;

  border: 1px solid #dbe2ea;

  border-radius: 12px;

  padding: 0 14px;

  outline: none;

  font-size: 14px;

  background: #fff;

  transition: 0.2s;
}

.form-group input:focus {
  border-color: #6366f1;

  box-shadow:
    0 0 0 4px rgba(99, 102, 241, 0.1);
}

.form-group input.ng-invalid.ng-touched {
  border-color: #ef4444;
}

.error {
  color: #dc2626;

  font-size: 11px;

  line-height: 1.4;
}


/* =========================================
   SUBJECTS
========================================= */

.subjects-header {
  padding-top: 10px;

  border-top: 1px solid #eef2f7;
}

.subject-row {
  display: grid;

  grid-template-columns:
    45px
    minmax(200px, 1fr)
    180px
    140px
    45px;

  gap: 15px;

  align-items: start;

  padding: 16px;

  margin-bottom: 12px;

  background: #f8fafc;

  border: 1px solid #e8edf3;

  border-radius: 16px;
}

.subject-number {
  width: 34px;
  height: 34px;

  display: flex;

  align-items: center;
  justify-content: center;

  background: #eef2ff;

  color: #4f46e5;

  border-radius: 10px;

  font-weight: 800;

  font-size: 13px;
}

.subject-status {
  padding-top: 31px;
}

.status {
  display: inline-flex;

  align-items: center;

  padding: 7px 11px;

  border-radius: 50px;

  font-size: 11px;

  font-weight: 800;

  white-space: nowrap;
}

.status.distinction {
  background: #fef3c7;
  color: #92400e;
}

.status.passed {
  background: #dcfce7;
  color: #166534;
}

.status.grace {
  background: #dbeafe;
  color: #1d4ed8;
}

.status.failed {
  background: #fee2e2;
  color: #b91c1c;
}

.delete-btn {
  margin-top: 29px;

  width: 40px;
  height: 40px;

  border: 0;

  border-radius: 10px;

  cursor: pointer;

  background: #fee2e2;

  color: #dc2626;

  transition: 0.2s;
}

.delete-btn:hover:not(:disabled) {
  background: #fecaca;

  transform: translateY(-2px);
}

.delete-btn:disabled {
  opacity: 0.35;

  cursor: not-allowed;
}


/* =========================================
   BUTTONS
========================================= */

.btn {
  border: 0;

  border-radius: 12px;

  padding: 12px 18px;

  font-size: 13px;

  font-weight: 700;

  cursor: pointer;

  transition: 0.2s;
}

.btn-primary {
  color: white;

  background:
    linear-gradient(
      135deg,
      #4f46e5,
      #7c3aed
    );

  box-shadow:
    0 8px 20px rgba(79, 70, 229, 0.25);
}

.btn-primary:hover {
  transform: translateY(-2px);

  box-shadow:
    0 12px 25px rgba(79, 70, 229, 0.35);
}

.btn-secondary {
  background: #f1f5f9;

  color: #334155;
}

.btn-secondary:hover {
  background: #e2e8f0;
}

.btn-add {
  background: #eef2ff;

  color: #4f46e5;
}

.btn-add:hover {
  background: #e0e7ff;
}

.form-actions {
  display: flex;

  justify-content: flex-end;

  gap: 12px;

  margin-top: 25px;
}


/* =========================================
   RULES
========================================= */

.rules-box {
  margin-top: 25px;

  padding: 20px;

  border-radius: 16px;

  background:
    linear-gradient(
      135deg,
      #f8fafc,
      #f1f5f9
    );

  border: 1px solid #e2e8f0;
}

.rules-box h3 {
  margin: 0 0 15px;

  font-size: 14px;
}

.rules-grid {
  display: grid;

  grid-template-columns:
    repeat(5, 1fr);

  gap: 10px;
}

.rules-grid div {
  text-align: center;

  padding: 13px 8px;

  background: white;

  border-radius: 12px;

  border: 1px solid #e5e7eb;
}

.rules-grid strong {
  display: block;

  color: #4f46e5;

  font-size: 15px;
}

.rules-grid span {
  display: block;

  margin-top: 4px;

  color: #64748b;

  font-size: 10px;
}


/* =========================================
   MARKSHEET HEADER
========================================= */

.marksheet {
  overflow: hidden;
}

.marksheet-header {
  display: flex;

  justify-content: space-between;

  align-items: flex-start;

  gap: 20px;

  padding-bottom: 25px;

  border-bottom: 1px solid #e5e7eb;
}

.certificate-label {
  font-size: 10px;

  letter-spacing: 2px;

  font-weight: 800;

  color: #6366f1;
}

.marksheet-header h2 {
  margin: 7px 0 4px;

  font-size: 28px;
}

.marksheet-header p {
  margin: 0;

  color: #64748b;

  font-size: 13px;
}


/* =========================================
   RESULT BADGE
========================================= */

.result-badge {
  padding: 12px 18px;

  border-radius: 50px;

  font-size: 12px;

  font-weight: 900;

  letter-spacing: 0.5px;
}

.result-pass {
  background: #dcfce7;
  color: #15803d;
}

.result-grace {
  background: #dbeafe;
  color: #1d4ed8;
}

.result-supplementary {
  background: #fef3c7;
  color: #a16207;
}

.result-fail {
  background: #fee2e2;
  color: #b91c1c;
}


/* =========================================
   STUDENT SUMMARY
========================================= */

.student-summary {
  display: grid;

  grid-template-columns:
    repeat(3, 1fr);

  gap: 15px;

  padding: 22px 0;
}

.student-summary div {
  padding: 15px;

  background: #f8fafc;

  border-radius: 12px;
}

.student-summary span {
  display: block;

  color: #64748b;

  font-size: 11px;

  margin-bottom: 5px;
}

.student-summary strong {
  font-size: 14px;
}


/* =========================================
   TABLE
========================================= */

.table-container {
  overflow-x: auto;
}

table {
  width: 100%;

  border-collapse: collapse;

  min-width: 700px;
}

thead {
  background: #f8fafc;
}

th {
  text-align: left;

  padding: 14px;

  font-size: 11px;

  color: #64748b;

  text-transform: uppercase;

  letter-spacing: 0.5px;
}

td {
  padding: 16px 14px;

  border-top: 1px solid #eef2f7;

  font-size: 13px;
}

.subject-name {
  font-weight: 700;

  color: #1e293b;
}

.out-of {
  color: #94a3b8;

  font-size: 11px;
}

.grace-mark {
  color: #2563eb;

  font-weight: 800;
}

.no-grace {
  color: #94a3b8;
}

.table-status {
  padding: 6px 10px;

  border-radius: 50px;

  font-size: 10px;

  font-weight: 800;

  white-space: nowrap;
}

.table-status.distinction {
  background: #fef3c7;
  color: #92400e;
}

.table-status.passed {
  background: #dcfce7;
  color: #166534;
}

.table-status.grace {
  background: #dbeafe;
  color: #1d4ed8;
}

.table-status.failed {
  background: #fee2e2;
  color: #b91c1c;
}


/* =========================================
   SUMMARY
========================================= */

.summary-grid {
  display: grid;

  grid-template-columns:
    repeat(4, 1fr);

  gap: 15px;

  margin-top: 25px;
}

.summary-card {
  padding: 20px;

  border-radius: 16px;

  background:
    linear-gradient(
      135deg,
      #f8fafc,
      #eef2ff
    );

  border: 1px solid #e2e8f0;
}

.summary-card span {
  display: block;

  color: #64748b;

  font-size: 11px;

  margin-bottom: 8px;
}

.summary-card strong {
  font-size: 25px;

  color: #1e293b;
}

.summary-card small {
  font-size: 12px;

  color: #94a3b8;
}


/* =========================================
   NOTICES
========================================= */

.grace-notice,
.supplementary-notice {
  display: flex;

  gap: 14px;

  margin-top: 20px;

  padding: 17px;

  border-radius: 14px;
}

.grace-notice {
  background: #eff6ff;

  border: 1px solid #bfdbfe;
}

.supplementary-notice {
  background: #fffbeb;

  border: 1px solid #fde68a;
}

.notice-icon {
  width: 35px;
  height: 35px;

  flex-shrink: 0;

  display: flex;

  align-items: center;
  justify-content: center;

  border-radius: 50%;

  background: white;

  font-weight: 900;
}

.grace-notice strong {
  color: #1d4ed8;
}

.supplementary-notice strong {
  color: #a16207;
}

.grace-notice p,
.supplementary-notice p {
  margin: 5px 0 0;

  color: #64748b;

  font-size: 12px;

  line-height: 1.5;
}


/* =========================================
   FOOTER
========================================= */

.marksheet-footer {
  margin-top: 25px;

  padding-top: 20px;

  border-top: 1px solid #e5e7eb;

  display: flex;

  align-items: center;

  justify-content: space-between;

  gap: 20px;
}

.marksheet-footer strong {
  font-size: 13px;
}

.marksheet-footer p {
  margin: 4px 0 0;

  font-size: 11px;

  color: #64748b;
}


/* =========================================
   RESPONSIVE
========================================= */

@media (max-width: 900px) {

  .form-grid {
    grid-template-columns: 1fr 1fr;
  }

  .subject-row {
    grid-template-columns:
      40px
      1fr
      150px
      120px
      40px;
  }

  .rules-grid {
    grid-template-columns:
      repeat(3, 1fr);
  }

  .summary-grid {
    grid-template-columns:
      repeat(2, 1fr);
  }
}


@media (max-width: 650px) {

  .header {
    padding: 20px;
  }

  .angular-badge {
    display: none;
  }

  .container {
    padding: 0 12px;
  }

  .card {
    padding: 20px;

    border-radius: 18px;
  }

  .form-grid {
    grid-template-columns: 1fr;
  }

  .section-heading,
  .subjects-header {
    align-items: flex-start;

    flex-direction: column;
  }

  .subject-row {
    grid-template-columns: 35px 1fr;

    gap: 12px;
  }

  .subject-row .form-group {
    grid-column: 2;
  }

  .subject-status {
    grid-column: 2;

    padding-top: 0;
  }

  .delete-btn {
    grid-column: 2;

    margin-top: 0;
  }

  .rules-grid {
    grid-template-columns:
      repeat(2, 1fr);
  }

  .student-summary {
    grid-template-columns: 1fr;
  }

  .summary-grid {
    grid-template-columns: 1fr 1fr;
  }

  .marksheet-header {
    flex-direction: column;
  }

  .marksheet-footer {
    align-items: flex-start;

    flex-direction: column;
  }

}

6. How the Reactive Form Works

The most important part for your students is this:

this.marksheetForm = new FormGroup({

  studentName: new FormControl(''),

  className: new FormControl(''),

  rollNumber: new FormControl(''),

  subjects: new FormArray([
    this.createSubject()
  ])

});

The interesting part is:

subjects: new FormArray([
    this.createSubject()
])

A FormArray allows us to create controls dynamically.


7. Dynamic Subject FormGroup

We create one subject using:

createSubject(): FormGroup {

  return new FormGroup({

    subjectName: new FormControl('', [
      Validators.required,
      Validators.minLength(2)
    ]),

    marks: new FormControl<number | null>(null, [
      Validators.required,
      Validators.min(0),
      Validators.max(100)
    ])

  });

}

So every subject looks like:

Subject
 ├── subjectName
 └── marks

8. Add Subject Dynamically

This is one of the most important Angular Reactive Forms concepts:

addSubject(): void {

  this.subjects.push(
    this.createSubject()
  );

}

If the student clicks:

+ Add Subject

Angular dynamically creates another:

FormGroup

inside:

FormArray

For example:

subjects
   |
   +-- Mathematics
   |     └── 85
   |
   +-- Physics
   |     └── 78
   |
   +-- Chemistry
         └── 62

9. Remove Subject

removeSubject(index: number): void {

  if (this.subjects.length === 1) {
    return;
  }

  this.subjects.removeAt(index);
}

The important method is:

removeAt(index)

10. Marks Validation

We use:

marks: new FormControl<number | null>(
  null,
  [
    Validators.required,
    Validators.min(0),
    Validators.max(100)
  ]
)

Therefore:

-1       ❌
0        ✅
25       ✅
33       ✅
75       ✅
100      ✅
101      ❌

11. Distinction Logic

The requirement is:

Marks greater than 75 = Distinction

Therefore:

isDistinction(index: number): boolean {

  const marks = Number(
    this.getSubject(index).get('marks')?.value ?? 0
  );

  return marks > 75;
}

Notice:

marks > 75

not:

marks >= 75

Therefore:

75 → Normal Pass

76 → Distinction

80 → Distinction

100 → Distinction

12. Failed Subject Logic

We define passing marks as:

33

So:

getFailedSubjectCount(): number {

  return this.subjects.controls.filter(subject => {

    const marks = Number(
      subject.get('marks')?.value ?? 0
    );

    return marks < 33;

  }).length;
}

For example:

Mathematics   80
Physics       65
Java          25

Result:

Failed subjects = 1

13. Supplementary Rule

The requirement is:

0 failed subjects
    ↓
PASS

1 failed subject
    ↓
SUPPLEMENTARY
or
PASS BY GRACE

2+ failed subjects
    ↓
FAIL

Implemented as:

getResult(): string {

  const failedSubjects =
    this.getFailedSubjectCount();

  if (failedSubjects === 0) {
    return 'PASS';
  }

  if (failedSubjects === 1) {

    if (this.canPassByGrace()) {
      return 'PASS BY GRACE';
    }

    return 'SUPPLEMENTARY';
  }

  return 'FAIL';
}

14. Grace Marks Logic

This is an important business-rule example.

Suppose:

Mathematics = 30

Minimum passing:

33

Required:

33 - 30 = 3

Since required grace is ≤ 5:

Grace = 3
Final = 33
Result = PASS BY GRACE

The method is:

getGraceMarks(index: number): number {

  const marks = Number(
    this.getSubject(index).get('marks')?.value ?? 0
  );

  if (marks >= 33) {
    return 0;
  }

  const requiredMarks = 33 - marks;

  if (requiredMarks <= 5) {
    return requiredMarks;
  }

  return 0;
}

15. Two Failed Subjects

Consider:

Java       30
Angular    25
SQL        70

There are:

2 failed subjects

Even though grace might theoretically help one subject, the business rule says:

More than one failed subject = FAIL

Therefore:

Result = FAIL

16. Example 1 — PASS

Enter:

Student: Rahul
Class: BCA 3rd Year
Roll: 101

Subjects:

Java        80
Angular     75
SQL         70
Python      85

The application displays:

Total Marks     310 / 400
Percentage      77.50%
Grade           B+
Failed Subjects 0
Result          PASS

And:

Java       🏆 Distinction
Angular    ✓ Pass
SQL        ✓ Pass
Python     🏆 Distinction

17. Example 2 — PASS BY GRACE

Enter:

Java        80
Angular     30
SQL         70
Python      65

Angular:

30

Required:

33

Grace:

3

So the marksheet displays:

Angular

Original Marks: 30
Grace: +3
Final Marks: 33
Status: Grace Pass

Overall:

PASS BY GRACE

18. Example 3 — SUPPLEMENTARY

Enter:

Java        80
Angular     27
SQL         70
Python      65

Angular requires:

33 - 27 = 6

Maximum grace:

5

Therefore:

Cannot pass using grace

Result:

SUPPLEMENTARY

19. Example 4 — FAIL

Enter:

Java        80
Angular     25
SQL         30
Python      70

Failed:

Angular
SQL

Failed subjects:

2

Therefore:

FAIL

Post a Comment

0 Comments