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
-
Student enters:
- Student Name
- Class Name
- Roll Number
- Dynamic Subject Name
- Dynamic Marks
- Marks must be 0–100.
- Subject marks can be added/removed dynamically.
- Marks > 75 → display Distinction.
- Subject marks < 33 → subject is failed.
-
Exactly 1 failed subject →
SUPPLEMENTARY. -
More than 1 failed subject →
FAIL. -
If exactly 1 subject is failed:
- Apply maximum 5 grace marks.
-
If failed subject can reach 33 →
PASS BY GRACE. -
Otherwise →
SUPPLEMENTARY.
-
If no subject is below 33 →
PASS. - Percentage and overall grade are calculated dynamically.
- 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 70There are:
2 failed subjectsEven though grace might theoretically help one subject, the business rule says:
More than one failed subject = FAILTherefore:
Result = FAIL
16. Example 1 — PASS
Enter:
Student: Rahul Class: BCA 3rd Year Roll: 101Subjects:
Java 80 Angular 75 SQL 70 Python 85The application displays:
Total Marks 310 / 400 Percentage 77.50% Grade B+ Failed Subjects 0 Result PASSAnd:
Java 🏆 Distinction Angular ✓ Pass SQL ✓ Pass Python 🏆 Distinction
17. Example 2 — PASS BY GRACE
Enter:
Java 80 Angular 30 SQL 70 Python 65Angular:
30Required:
33Grace:
3So the marksheet displays:
Angular Original Marks: 30 Grace: +3 Final Marks: 33 Status: Grace PassOverall:
PASS BY GRACE
18. Example 3 — SUPPLEMENTARY
Enter:
Java 80 Angular 27 SQL 70 Python 65Angular requires:
33 - 27 = 6Maximum grace:
5Therefore:
Cannot pass using graceResult:
SUPPLEMENTARY
19. Example 4 — FAIL
Enter:
Java 80 Angular 25 SQL 30 Python 70Failed:
Angular SQLFailed subjects:
2Therefore:

0 Comments
POST Answer of Questions and ASK to Doubt