Skip to content

Components refactor#233

Draft
stusherwin wants to merge 7 commits into
mainfrom
components-refactor
Draft

Components refactor#233
stusherwin wants to merge 7 commits into
mainfrom
components-refactor

Conversation

@stusherwin

@stusherwin stusherwin commented May 28, 2026

Copy link
Copy Markdown
Collaborator

This PR is the result of the Components spike, resulting in a number of proposed changes:

1. <tabbed-view> tag helper

This encapsulates the switchable tabs within an individual measure as displayed on a page:

<tabbed-view html-prefix="some-measure">
  <tab-content id="tab1" name="Tab 1">
    <p>This is content for Tab 1</p>
  </tab-content>
  <tab-content id="tab2" name="Tab 2">
    <p>This is the content for Tab 2</p>
  </tab-content>
</tabbed-view>

This produces the HTML (simplified):

<div class="govuk-tabs">
  <ul class="govuk-tabs__list">
    <li class="govuk-tabs__list-item>
      <a class="govuk-tabs__tab" href="#some-measure-tab1">Tab 1</a>
    </li>
    <li class="govuk-tabs__list-item">
      <a class="govuk-tabs__tab" href="#some-measure-tab2">Tab 2</a>
    </li>
  </ul>
  <div class="govuk-tabs__panel" id="some-measure-tab1">
    <p>This is content for Tab 1</p>
  </div>
  <div class="govuk-tabs__panel govuk-tabs__panel--hidden" id="some-measure-tab2">
    <p>This is the content for Tab 2</p>
  </div>
</div>

Which results in the following:
image

2. Measure components (partials)

A number of measure-related partials have been introduced which split up the view code needed to render the component parts of a measure displayed on the page.

_Measure

Partial: _Measure
View model: MeasureViewModel

Renders the complete measure on the page, with its submeasures:

image

Note the submeasures (three year average, top performers etc.) are rendered dynamically as different <tab-content> elements. Different types of measures can have different submeasures, for instance a KS4 headline measure for a school will show Three year average, Top performers, Year by Year and Table submeasures, whereas the same KS4 headline measure when comparing a school with one of its similar schools will only show Three year average, Year by Year and Table submeasures.

_MeasureFilters

Partial: _MeasureFilters
View model: MeasureInfoViewModel

Dynamically renders any filter dropdowns attached to the measure:

image

_MeasureThreeYearAverageChart

Partial: _MeasureThreeYearAverageChart
View model: ThreeYearAverageSubMeasureViewModel

Renders the three year average submeasure:
image

Note the number of averages and the labels associated are now dynamic, for instance when rendered within a measure on a school page, the school average, similar schools average, LA average and England average are shown. Whereas on a school comparison page, the current school average, similar school average and England average are shown.

_MeasureTopPerformers

Partial: _MeasureTopPerformers
View model: TopPerformersSubMeasureViewModel

Renders the top perfomers submeasure:
image

_MeasureYearByYearChart

Partial: _MeasureYearByYearChart
View model: YearByYearSubMeasureViewModel

Renders the year-by-year submeasure:
image

Note the year-by-year series and the labels associated are now dynamic, for instance when rendered within a measure on a school page, the school average, similar schools average, LA average and England average are shown. Whereas on a school comparison page, the current school average, similar school average and England average are shown.

_MeasureTable

Partial: _MeasureTable
View model: TableSubMeasureViewModel

Renders the table submeasure:
image

Note the rows and row headers are now dynamic, for instance when rendered within a measure on a school page, the school average, similar schools average, LA average and England average are shown. Whereas on a school comparison page, the current school average, similar school average and England average are shown.

3. Measure filtering

To make measures more generic, the functionality for filtering has been simplified. Previously there was a separate controller action for each measure type (e.g. English and Maths, Destinations, Attendance, GCSE Core Subject), which applied the filter selected in the dropdown and returned a JSON object containing all the values for each submeasure. This was then applied to the page using some custom JavaScript code which altered the values of the HTML elements for each submeasure.

This has been changed so that there is now a single action for each measure page. This action also handles filtering for each measure within the page - filters can now be applied at the page level with a querystring, so for example the URL /school/136994/ks4-core-subjects?english-literature:grade=5&biology:grade=7 will apply filters simultaneously to the English literature and Biology measures:

image

The JavaScript code to apply a filter when selected now just requests the whole page again via the Fetch API with the new filter applied to the querystring, and replaces the content of the measure with the same id from the response content.

This also means that when we come to ensure filtering functionality still works without JavaScript enabled, it should be a simple change to allow a form to submit the whole page with the selected filters applied.

Note there is a small issue that still needs resolving, where if the user selects a tab within a measure, and then applies the filter for that measure, it will forget what tab they were on. This should be fairly straightforward to fix, I just ran out of time in this spike.

4. Use case refactoring

In order to implement the UI changes, a couple of refactorings needed to be done at the use case level:

1. Pull filtering logic out of the controllers and into the use cases

This has a dual purpose:

  • making the controllers simpler, so that they only have to deal with marshalling data from the use case response into view models; and
  • bringing business logic (filtering measures by grade, etc is very much business logic) into the use cases so it can be fully covered by use case tests instead of UI-level tests

The relevant use cases (GetSchoolKs4HeadlineMeasures, GetSchoolKs4CoreSubjects, GetSchoolComparisonKs4HeadlineMeasures, GetSchoolComparisonKs4CoreSubjects) now take an additional IDictionary<string, string>? FilterBy parameter in their requests, and pass the filters on to each individual Measure object to do its own filtering.

2. Extract out the concept of a Measure into its own domain object, and have each use case return a list of these Measure objects.

The previous version of the code did not have a concept of a measure modelled explicitly, and each use case had a different way of querying the measure data and structuring it in its responses. Each measure now is represented by the Measure domain object, which represents a collection of SubMeasures.

Each Measure and SubMeasure class has the responsibility of building out its own data depending on whether it is a "school" measure or a "school comparison" measure (the ForSchool() and ForSchoolComparison() static methods).

Measures also now have builder methods which are grouped into the Ks4HeadlineMeasures and Ks4CoreSubjects static classes:

Ks4HeadlineMeasures
{
  Attainment8
  {
    static Measure ForSchool()
    static Measure ForSchoolComparison()
  }
  EnglishAndMaths
  {
    static Measure ForSchool()
    static Measure ForSchoolComparison()
  }
  Destinations
  {
    static Measure ForSchool()
    static Measure ForSchoolComparison()
  }
}

Ks4CoreSubjects
{
  EnglishLanguage
  {
    static Measure ForSchool()
    static Measure ForSchoolComparison()
  }
  EnglishLiterature
  {
    static Measure ForSchool()
    static Measure ForSchoolComparison()
  }
  Biology
  {
    static Measure ForSchool()
    static Measure ForSchoolComparison()
  }
  ...
}

These can then be called from the use cases as follows:

return new GetSchoolKs4HeadlineMeasuresResponse(
    schoolDetails,
    similarSchools.Length,
    Measures.Ks4HeadlineMeasures.Attainment8.ForSchool(
        schoolData,
        similarSchools,
        filterBy),
    Measures.Ks4HeadlineMeasures.EnglishAndMaths.ForSchool(
        schoolData,
        similarSchools,
        filterBy),
    Measures.Ks4HeadlineMeasures.Destinations.ForSchool(
        schoolData,
        similarSchools,
        filterBy));

5. JavaScript file restructure

The new implementation had a bug where the current tab in a measure would not be remembered when a new filter value was applied. The fix would involve some changes to the similar-schools-tabs-mobile-fix.js file, and while investigating this it seemed that the purpose of this file was to override the mobile behaviour of the tabs to only show the currently selected tab rather than list out all the tabs in the page. This was implemented by effectively re-implementing the behaviour of the govuk-tabs component. However it seemed that this could be replaced by a simple override to the behaviour of the component - implemented in the mobile-collapsed-tabs module](https://github.com/DFE-Digital/sap-sector/pull/233/changes#diff-13f6c8a3203c56d77df17153eeebd28e6d38db0f5879d60345a8889f1decde5c).

In order to accomplish this, the structure of the existing JS files needed to be changed to module files rather than standalone scripts. This then meant that modules could be included within other modules, for example the measure-filters module now includes the mobile-collapsed-tabs module and calls MobileCollapsedTabs.selectTabById() to re-select the current tab after the filters have been applied.

Summary

The changes above have been implemented in the following pages (using example URNs):

School headline measures/core subjects pages

/school/136994/ks4-headline-measures
/school/136994/ks4-core-subjects

School comparison headline measures/core subjects pages

/school/136994/view-similar-schools/137921/ks4-core-subjects
/school/136994/view-similar-schools/137921/ks4-headline-measures

I think the best approach if we agree this is the way to go would be to implement these changes bit by bit (say a separate PR for each page affected) - that way we can review as we go and ensure each change is fully tested.

@stusherwin stusherwin added the deploy deploy application label May 28, 2026
@stusherwin stusherwin marked this pull request as draft May 28, 2026 20:01
@stusherwin stusherwin force-pushed the components-refactor branch from 16759b0 to 1642993 Compare May 28, 2026 20:07
@github-actions

Copy link
Copy Markdown

Deployments

Review app is available at these URLs:
https://get-school-improvement-insights-pr-233.test.teacherservices.cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deploy deploy application

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant