From a195ac18c4ed9e3b83e022c3d339f79896fb95c8 Mon Sep 17 00:00:00 2001 From: Abban Dunne Date: Fri, 28 Apr 2023 07:11:18 +0200 Subject: [PATCH 1/6] Add BannerConductor doc --- docs/BannerConductor.md | 26 +++++++++++++++++++ .../BannerConductor/BannerConductor.vue | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 docs/BannerConductor.md diff --git a/docs/BannerConductor.md b/docs/BannerConductor.md new file mode 100644 index 000000000..15f3b4fe0 --- /dev/null +++ b/docs/BannerConductor.md @@ -0,0 +1,26 @@ +# Banner Conductor + +The Banner Conductor is the root Vue component and is responsible for handling the top level banner states, and is the gateway for interactions between the banners and the pages they are displayed on. It is largely invisible to the developers. + +Internally it uses a [Finite State Machine](https://en.wikipedia.org/wiki/Finite-state_machine) (FSM) with the following state flow: + +```mermaid +stateDiagram-v2 + [*] --> Initial + Initial --> Pending + Pending --> Showing + Pending --> NotShown: has size issue + Showing --> Visible + Visible --> Hidden + NotShown --> [*] + Hidden --> [*] +``` + +The states are as follows: + +* **Initial** This state does nothing, it is used as a default state when the FSM is instantiated. +* **Pending** This pre-sets the banner height on the Page and starts the banner display timer. +* **NotShown** When there is a size issue, or if the user interacts with certain elements on the page while the banner display timer is running the FSM will move to this state at the timer end. It will then mark the banner as not shown and fire tracking events. +* **Showing** This is the banner transition phase. It is in this state when the banner is animating into the page. +* **Visible** The banner is now finished transition and is visible to the user on the page. It will update the display counts and trigger the displayed tracking events when entered. _Note: This does not control which part of the banner is visible, that is the responsibility of the banner components._ +* **Hidden** When the user has closed the banner. \ No newline at end of file diff --git a/src/components/BannerConductor/BannerConductor.vue b/src/components/BannerConductor/BannerConductor.vue index 9fdc79bc9..e3686cd28 100644 --- a/src/components/BannerConductor/BannerConductor.vue +++ b/src/components/BannerConductor/BannerConductor.vue @@ -70,7 +70,8 @@ async function onCloseHandler( source: CloseSources ): Promise { background: #ffffff; z-index: 1000; } -.wmde-banner-closed { +.wmde-banner--not-shown, +.wmde-banner--closed { display: none; } From f9ea5ef7e0a36ae926e2a2b32c1eac7f52880b36 Mon Sep 17 00:00:00 2001 From: Abban Dunne Date: Fri, 28 Apr 2023 09:28:04 +0200 Subject: [PATCH 2/6] Add Page doc --- docs/Page.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/Page.md diff --git a/docs/Page.md b/docs/Page.md new file mode 100644 index 000000000..36469784c --- /dev/null +++ b/docs/Page.md @@ -0,0 +1,35 @@ +# The Page Classes + +We display banners on wikipedia.org and wikipedia.de. These sites have different html markup, have a different campaign configuration structure, and have different ways of logging events. We use a Page interface with 2 implementations which ensures our code is compatible with these 2 environments. + +They are used for: + +* Mounting the banners in the correct place. +* Tracking events. +* Passing the calculated banner height from our banner javascript to css. + +The BannerConductor is then given one of these implementations to perform actions as needed. + +## PageOrg (Used on wikipedia.org) +This implementation is for interacting with wikipedia.org and is a little complex. + +### The mw Object +Wikipedia.org provides this javascript object on the page that the PageOrg class uses for various things such as: + +* Discovering which skin the user has active. +* Discovering the current namespace of the page the banner is being showed on. +* Notifying Central Notice that a user has seen or interacted with a banner. +* Tracking events with Wikipedia's tracking tools. + +### Skins + +There are multiple skins that our banners can appear on: + +* **Vector** This skin is the default on dewiki. +* **Vector 2022** This skin is the default on enwiki. +* **Minerva** This is the default mobile skin. + +Each one of these skins has different markup and the users will interact with different parts of the skins that may prevent banner display so this needs to be set up per-skin. + +## PageDe (Used on wikipedia.de) +This is the simplest of the 2 implementations. We use it only to mount the banner and send tracking events to our Matomo analytics. From febed02fa3ff872b0513516318dc109298ff315c Mon Sep 17 00:00:00 2001 From: Abban Dunne Date: Fri, 28 Apr 2023 09:58:27 +0200 Subject: [PATCH 3/6] Add DynamicContent doc --- docs/DynamicContent.md | 24 ++++++++++++++++++++++ src/utils/DynamicContent/DynamicContent.ts | 9 +++----- 2 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 docs/DynamicContent.md diff --git a/docs/DynamicContent.md b/docs/DynamicContent.md new file mode 100644 index 000000000..6de286af8 --- /dev/null +++ b/docs/DynamicContent.md @@ -0,0 +1,24 @@ +# Dynamic Content + +The campaign text is mostly static but contains some dynamic content. This content consists of things like: + +* The number of days left in the campaign. +* The current day name. +* The average donation. +* The average number of donors. + +This content is used to generate sentences that are displayed as part of the banner text content, and as values for the progress bar. + +## Structure + +### Generators +These are for building the sentences and progress bar items. There is a single generator per dynamic text item. + +### Formatters +These are tools used by the generators for localising numbers depending on if the banners are in German or English. + +### Campaign Projection +The current campaign stats are only uploaded every few days during the campaign. The stats are then projected forward from the last upload time in order to keep the data as current as possible. The CampaignProjection is responsible for doing this projection. + +### Dynamic Campaign Text +The DynamicCampaignText class acts as a factory that instantiates the various generators. diff --git a/src/utils/DynamicContent/DynamicContent.ts b/src/utils/DynamicContent/DynamicContent.ts index fe96dbe87..462f9bf17 100644 --- a/src/utils/DynamicContent/DynamicContent.ts +++ b/src/utils/DynamicContent/DynamicContent.ts @@ -1,3 +1,5 @@ +import { DynamicProgressBarContent } from '@src/utils/DynamicContent/DynamicProgressBarContent'; + export interface DynamicContent { currentDayName: string; currentDate: string; @@ -7,10 +9,5 @@ export interface DynamicContent { donorsNeededSentence: string; goalDonationSum: string; overallImpressionCount: number; - progressBarContent: { - percentageTowardsTarget: number, - donationTarget: string, - amountDonated: string, - amountNeeded: string - }; + progressBarContent: DynamicProgressBarContent; } From ec9bbca028d3b02a1d08b1115ba1cdadfa71c2e3 Mon Sep 17 00:00:00 2001 From: Abban Dunne Date: Tue, 2 May 2023 06:53:58 +0200 Subject: [PATCH 4/6] Add change suggestions to docs --- docs/BannerConductor.md | 18 +++++++++++++++--- docs/DynamicContent.md | 2 +- docs/Page.md | 6 +++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/BannerConductor.md b/docs/BannerConductor.md index 15f3b4fe0..34ca8e94d 100644 --- a/docs/BannerConductor.md +++ b/docs/BannerConductor.md @@ -1,8 +1,20 @@ # Banner Conductor -The Banner Conductor is the root Vue component and is responsible for handling the top level banner states, and is the gateway for interactions between the banners and the pages they are displayed on. It is largely invisible to the developers. +The Banner Conductor is the root Vue component and is responsible for handling the top level banner states, and is the gateway for interactions between the banners and the [pages](Page.md) they are displayed on. It is largely invisible to the developers. -Internally it uses a [Finite State Machine](https://en.wikipedia.org/wiki/Finite-state_machine) (FSM) with the following state flow: +Internally it uses a [Finite State Machine](https://en.wikipedia.org/wiki/Finite-state_machine) (FSM). + +These states perform various tasks such as: + +* Measuring impressions +* Sending tracking events +* Handling page resize events + +You can get an overview of each state's dependencies by looking in the `StateFactory`. + +Each state has 2 lifecycle methods `enter()` and `exit()` which are called automatically when the state is entered and exited. These methods return promises and allow the states to be run in an asynchronous manner, for example the `Pending` state will only resolve its promise when the banner delay timer has completed. + +This is the state flow: ```mermaid stateDiagram-v2 @@ -23,4 +35,4 @@ The states are as follows: * **NotShown** When there is a size issue, or if the user interacts with certain elements on the page while the banner display timer is running the FSM will move to this state at the timer end. It will then mark the banner as not shown and fire tracking events. * **Showing** This is the banner transition phase. It is in this state when the banner is animating into the page. * **Visible** The banner is now finished transition and is visible to the user on the page. It will update the display counts and trigger the displayed tracking events when entered. _Note: This does not control which part of the banner is visible, that is the responsibility of the banner components._ -* **Hidden** When the user has closed the banner. \ No newline at end of file +* **Hidden** When the user has closed the banner. diff --git a/docs/DynamicContent.md b/docs/DynamicContent.md index 6de286af8..5cb3f99ed 100644 --- a/docs/DynamicContent.md +++ b/docs/DynamicContent.md @@ -18,7 +18,7 @@ These are for building the sentences and progress bar items. There is a single g These are tools used by the generators for localising numbers depending on if the banners are in German or English. ### Campaign Projection -The current campaign stats are only uploaded every few days during the campaign. The stats are then projected forward from the last upload time in order to keep the data as current as possible. The CampaignProjection is responsible for doing this projection. +The campaign team measures the progress of the campaign and uploads the current measurements every few days. We use these measurements to project the numbers displayed in the dynamic text. The calculation starts at the last upload time up to the point in time when the user sees the banner. The CampaignProjection is responsible for doing the calculations for this projection. ### Dynamic Campaign Text The DynamicCampaignText class acts as a factory that instantiates the various generators. diff --git a/docs/Page.md b/docs/Page.md index 36469784c..c95498ca3 100644 --- a/docs/Page.md +++ b/docs/Page.md @@ -1,6 +1,6 @@ # The Page Classes -We display banners on wikipedia.org and wikipedia.de. These sites have different html markup, have a different campaign configuration structure, and have different ways of logging events. We use a Page interface with 2 implementations which ensures our code is compatible with these 2 environments. +We display banners on wikipedia.org and wikipedia.de. These sites have different HTML markup, have a different campaign configuration structure, and have different ways of logging events. We use a Page interface with 2 implementations which ensures our code is compatible with these 2 environments. They are used for: @@ -8,13 +8,13 @@ They are used for: * Tracking events. * Passing the calculated banner height from our banner javascript to css. -The BannerConductor is then given one of these implementations to perform actions as needed. +In our entry points we give the BannerConductor one of these implementations to perform actions as needed. ## PageOrg (Used on wikipedia.org) This implementation is for interacting with wikipedia.org and is a little complex. ### The mw Object -Wikipedia.org provides this javascript object on the page that the PageOrg class uses for various things such as: +ikipedia.org provides the global `mw` JavaScript object. For PageOrg we wrap the object in a `MediaWiki` interface and use it for things like: * Discovering which skin the user has active. * Discovering the current namespace of the page the banner is being showed on. From 7c2eab6ae056c4b512b705811abd1d4bb6494c8b Mon Sep 17 00:00:00 2001 From: Abban Dunne Date: Tue, 2 May 2023 09:51:28 +0200 Subject: [PATCH 5/6] Add FormController doc --- ...ltiStepDonationForm-and-FormControllers.md | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 docs/MultiStepDonationForm-and-FormControllers.md diff --git a/docs/MultiStepDonationForm-and-FormControllers.md b/docs/MultiStepDonationForm-and-FormControllers.md new file mode 100644 index 000000000..3eb13a8a1 --- /dev/null +++ b/docs/MultiStepDonationForm-and-FormControllers.md @@ -0,0 +1,94 @@ +# Multi Step Donation Form and Form Controllers + +Since the 2022 campaign a lot of tests have been focused on multistep forms, each with different flows. These tests are different enough functionally that they would require a new `DonationForm` component for each. + +In order to avoid duplicating components per-test we opted to instead create a flexible `MultiStepDonationForm` that is provided with different implementations of a `FormController` which contains the logic that determines which form page to show, based on the user interaction with the current form page. + +This module consists of the following parts: + +## `MultiStepDonationForm.vue` + +This initialises and presents the form: +1. It connects the individual form pages with the callbacks of the `FormController` to handle the flow of the submissions. +2. It wraps the form pages in a `Slider` component and starts the slides at page 1. +3. From then on it acts as a bus between the `FormController`, `Slider` and the `FormSteps`. + +## `FormModel` + +This is a [Vuejs composable](https://vuejs.org/guide/reusability/composables.html) and contains the form state. It is globally available. This contains only the data that will be posted to the Fundraising Application and should not be modified for a single test. + +## `FormController` + +There are multiple implementations of this (one per type of form) and it is responsible for handling the entire flow of the submission. Events come from the `FormSteps` (with optional extra data), the `FormController` handles the logic of the event and calls one of the callbacks the `MultiStepDonationForm` provided to continue. + +The events it expects are: +* `submit` This is when a sub-form has been submitted. The data passed has already been validated. +* `next` This is called when the user clicks a link or button to move skip the current form and go to the next step. +* `previuous` This is called when the user clicks the back button. + +## `FormSteps` + +Each form consists of one or more steps. Each step: + +* Handles its own validation. +* Emits a `submit` or `next` or `previous` event with data. These events are sent to the `FormController` and allow it to decide what to do next. +* Optionally resets state when they are entered or exited. For example if a user hits the back button we reset some forms to their default state, and we expect the user to fill them out again to proceed. +* Optionally modify the `FormModel` directly. The `FormModel` contains only data that is to be posted to the Fundraising Application. That means that forms that contains fields to set this data can set it directly. Some forms, however, are for modifying this data as a side effect of the `submit`, `next` and `previous` actions. These forms pass the extra data to the `FormController` and it decides what should be changed in the `FormModel` + +## `SubmitForm` + +This is a hidden form. It contains the values from the `FormModel` as hidden fields and is submitted by the `MultiStepDonationForm` when `FormController` invokes the `onSubmit` callback. This data is the POSTed to the Fundraising Application as a standard HTTP POST request. + +## In Summary + +* The `MultistepDonationForm` acts like an event bus and keeps track which page is the current one. +* The `FormModel` contains the form state values. +* The `FormController` handles the main form submission logic. +* The `FormSteps` handle their own validation and fire submit/next/previous events. +* Some `FormSteps` modify the model state directly some pass that data to the `FormController` for extra computing. +* The `SubmitForm` has hidden form fields with `FormModel` values and POSTs to the Fundraising Application. + +```mermaid +stateDiagram-v2 + FormModel: Globally available FormModel + FormController --> MultiStepDonationForm + MultiStepDonationForm --> FormStepOne + MultiStepDonationForm --> FormStepTwo + MultiStepDonationForm --> FormStepThree + MultiStepDonationForm --> SubmitForm: Used to post data to Fundraising Application +``` + +Below is an example happy path user flow. + +```mermaid +sequenceDiagram + participant FormController + participant MultiStepDonationForm + participant FormStep1 + participant FormStep2 + participant SubmitForm + + MultiStepDonationForm -->> FormController: add callback to onNext + MultiStepDonationForm -->> FormController: add callback to onPrevious + MultiStepDonationForm -->> FormController: add callback to onGoToStep + MultiStepDonationForm -->> FormController: add callback to onSubmit + + FormStep1 ->> MultiStepDonationForm: user submits step 1 + MultiStepDonationForm ->> FormController: pass submit to controller + FormController ->> MultiStepDonationForm: calls onNext callback + MultiStepDonationForm ->> FormStep2: shows next step + + FormStep2 ->> MultiStepDonationForm: user submits step 2 + MultiStepDonationForm ->> FormController: pass submit to controller + FormController ->> MultiStepDonationForm: calls onSubmit callback + MultiStepDonationForm ->> SubmitForm: posts form +``` + +## Notes for the future + +This module isn't perfect. Depending on the direction of the future campaign tests we should improve some things about it. + +* The form user flows are badly designed. For example, we don't know the reasoning why some forms clear their own state on back. This needs to be clarified before a redesign happens. +* `FormControllers` These are where we put the smelly code. Event handling is done per sub-form using switch statements to branch the logic. This could be improved somewhat. +* Some of the FormControllers are very similar. We might extract them into shared classes for common sets of form steps. +* `FormSteps` Some of these modify the `FormModel` directly, some pass extra data so the `FormController` can modify this data. These steps could be changed to each contain their own state and the `FormController` becomes responsible for changing all `FormModel` state, meaning the `FormModel` no longer needs to be so global. From ab7feda6656a044f768729d6c9a51e1ca035f2eb Mon Sep 17 00:00:00 2001 From: Gabriel Birke Date: Wed, 24 May 2023 16:29:30 +0200 Subject: [PATCH 6/6] Change MultiStepDonationForm docs Remove the descriptions for FormController and describe the new StepControllers instead. --- docs/MultiStepDonationForm-and-Controllers.md | 102 ++++++++++++++++++ ...ltiStepDonationForm-and-FormControllers.md | 94 ---------------- 2 files changed, 102 insertions(+), 94 deletions(-) create mode 100644 docs/MultiStepDonationForm-and-Controllers.md delete mode 100644 docs/MultiStepDonationForm-and-FormControllers.md diff --git a/docs/MultiStepDonationForm-and-Controllers.md b/docs/MultiStepDonationForm-and-Controllers.md new file mode 100644 index 000000000..c02e47ae3 --- /dev/null +++ b/docs/MultiStepDonationForm-and-Controllers.md @@ -0,0 +1,102 @@ +# Multi Step Donation Form and Form Controllers + +Since the 2022 campaign, a lot of tests had multistep forms, each one with a different flow. These tests are different enough functionally that they would require a new `DonationForm` component for each. + +In order to avoid duplicating components per-test we opted to instead create a flexible `MultiStepDonationForm` that gets each form step as a named slot. Each named slot also gets an implementation of a `StepController`, which contains the logic that determines which form page to show, based on the user interaction with the current form page. + +This module consists of the following parts: + +## `MultiStepDonationForm.vue` + +This initialises and presents the form: +1. It connects the individual form pages to the `StepController` instances to handle the flow of the submissions. +2. It wraps the `FormStep` components in its slots in a `Slider` component and starts the slides at the first slide. +3. From then on it acts as a bus between the `StepController` instances, `Slider` and the `FormSteps`. + +## `FormModel` + +This is a [Vuejs composable](https://vuejs.org/guide/reusability/composables.html) and contains the form state. It is globally available. This contains only the data that will be posted to the Fundraising Application and should not be modified for a single test. + +## `FormSteps` + +A banner form consists of one or more step components (components containing with form elements). Each step: + +* Is in the directory `src/components/DonationForm/Forms` +* Handles its own validation. +* Emits a `submit` or `previous` event with data. The `MultiStepDonationForm` passes the events to the `StepController` and allows it to decide what to do next. +* Optionally resets state when they are entered or exited. For example if a user hits the back button we reset some forms to their default state, and we expect the user to fill them out again to proceed. The form can watch its `isCurrent` property to detect when it becomes the current step. +* Optionally modify the `FormModel` directly. The `FormModel` contains only data that will be posted to the Fundraising Application. Some form steps, like the `MainDonationForm` have fields that directly correspond to `FormModel` properties and will change it directly. Other forms, however, are for modifying this data as a side effect of the `submit` and `previous` actions. These forms pass the extra data to their `StepController` and it will decide what should change in the `FormModel`. + +## `StepController` + +There are multiple implementations of this interface, at least one per form step. It is responsible for handling the flow of the submission, instructing the `MultiStepDonationForm` either to submit or go to a different step. + +Each controller instance has two methods, called by `MultiStepDonationForm`: +* `submit` This is called when a sub-form has been submitted. The data passed has already been validated. It may get extra data about the user input from the form step. +* `previous` This is called when the user clicks the back button. + +The `MultiStepDonationForm` passes the following callbacks into each StepController: +* `goToStep` makes the `MultiStepDonationForm` go to a different page with the specified name. `MultiStepDonationForm` builds an internal mapping between the step names and the step index needed by its `Slider` component. +* `submit` makes the `MultiStepDonationForm` submit the current values of `FormModel` via the `SubmiForm` (see below). + +Each `StepController` has a factory function that provides it with the slot names of the possible other steps it can go to. +The `StepController` factory functions follow the following naming schema: +`create` + +* `` can either be `Intermediate` (meaning the step will not submit, but go to a different form step) or `Submittable` (meaning the controller will call `submit` or *may* go to a different step in some cases). +* `` is the name of the form component, e.g. `MainDonationForm` or `AddressTypeForm`. +* `` is an optional description of the controller behavior, e.g, `SinglePage` + +Example factory functions: `createSubmittableAddressType`, `createIntermediateUpgradeToYearly`, `createSubmittableMainDonationFormSinglePage` + +## `SubmitForm` + +This is a hidden form. It contains the values from the `FormModel` as hidden fields and is submitted by the `MultiStepDonationForm` when a `StepController` invokes the `submit` callback. This data is the POSTed to the Fundraising Application as a standard HTTP POST request. + +## In Summary + +* The `MultistepDonationForm` acts like an event bus and keeps track which page is the current one. +* The `FormModel` contains the form state values. +* The `StepControllers` handle the form submission logic. +* The `FormSteps` handle their own validation and fire submit/previous events. +* Some `FormSteps` modify the model state directly or pass their data to the `StepController` for extra computing. +* The `SubmitForm` has hidden form fields with `FormModel` values and POSTs to the Fundraising Application. + +```mermaid +stateDiagram-v2 + FormModel: Globally available FormModel + StepControllers --> MultiStepDonationForm + MultiStepDonationForm --> FormStepOne + MultiStepDonationForm --> FormStepTwo + MultiStepDonationForm --> FormStepThree + MultiStepDonationForm --> SubmitForm: Used to post data to Fundraising Application +``` + +Below is an example happy path user flow. + +```mermaid +sequenceDiagram + participant MultiStepDonationForm + participant FormStep1 + participant StepController1 + participant FormStep2 + participant StepController2 + participant SubmitForm + + FormStep1 ->> MultiStepDonationForm: user submits step 1 + MultiStepDonationForm ->> StepController1: call `submit` on controller + StepController1 ->> MultiStepDonationForm: calls `goToStep` callback + MultiStepDonationForm ->> FormStep2: shows next step + + FormStep2 ->> MultiStepDonationForm: user submits step 2 + MultiStepDonationForm ->> StepController2: calls `submit` on controller + StepController2 ->> MultiStepDonationForm: calls `submit` callback + MultiStepDonationForm ->> SubmitForm: posts form +``` + +## Notes for the future + +This module isn't perfect. Depending on the direction of the future campaign tests we might improve some things about it. + +* The form user flows are badly designed. For example, we don't know the reasoning why some forms clear their own state on back. This needs to be clarified before a redesign happens. +* `FormSteps` Some of these modify the `FormModel` directly, some pass extra data so the `StepController` can modify this data. These steps could be changed to each contain their own state and the `StepController` becomes responsible for changing all `FormModel` state, meaning the `FormModel` no longer needs to be so global. diff --git a/docs/MultiStepDonationForm-and-FormControllers.md b/docs/MultiStepDonationForm-and-FormControllers.md deleted file mode 100644 index 3eb13a8a1..000000000 --- a/docs/MultiStepDonationForm-and-FormControllers.md +++ /dev/null @@ -1,94 +0,0 @@ -# Multi Step Donation Form and Form Controllers - -Since the 2022 campaign a lot of tests have been focused on multistep forms, each with different flows. These tests are different enough functionally that they would require a new `DonationForm` component for each. - -In order to avoid duplicating components per-test we opted to instead create a flexible `MultiStepDonationForm` that is provided with different implementations of a `FormController` which contains the logic that determines which form page to show, based on the user interaction with the current form page. - -This module consists of the following parts: - -## `MultiStepDonationForm.vue` - -This initialises and presents the form: -1. It connects the individual form pages with the callbacks of the `FormController` to handle the flow of the submissions. -2. It wraps the form pages in a `Slider` component and starts the slides at page 1. -3. From then on it acts as a bus between the `FormController`, `Slider` and the `FormSteps`. - -## `FormModel` - -This is a [Vuejs composable](https://vuejs.org/guide/reusability/composables.html) and contains the form state. It is globally available. This contains only the data that will be posted to the Fundraising Application and should not be modified for a single test. - -## `FormController` - -There are multiple implementations of this (one per type of form) and it is responsible for handling the entire flow of the submission. Events come from the `FormSteps` (with optional extra data), the `FormController` handles the logic of the event and calls one of the callbacks the `MultiStepDonationForm` provided to continue. - -The events it expects are: -* `submit` This is when a sub-form has been submitted. The data passed has already been validated. -* `next` This is called when the user clicks a link or button to move skip the current form and go to the next step. -* `previuous` This is called when the user clicks the back button. - -## `FormSteps` - -Each form consists of one or more steps. Each step: - -* Handles its own validation. -* Emits a `submit` or `next` or `previous` event with data. These events are sent to the `FormController` and allow it to decide what to do next. -* Optionally resets state when they are entered or exited. For example if a user hits the back button we reset some forms to their default state, and we expect the user to fill them out again to proceed. -* Optionally modify the `FormModel` directly. The `FormModel` contains only data that is to be posted to the Fundraising Application. That means that forms that contains fields to set this data can set it directly. Some forms, however, are for modifying this data as a side effect of the `submit`, `next` and `previous` actions. These forms pass the extra data to the `FormController` and it decides what should be changed in the `FormModel` - -## `SubmitForm` - -This is a hidden form. It contains the values from the `FormModel` as hidden fields and is submitted by the `MultiStepDonationForm` when `FormController` invokes the `onSubmit` callback. This data is the POSTed to the Fundraising Application as a standard HTTP POST request. - -## In Summary - -* The `MultistepDonationForm` acts like an event bus and keeps track which page is the current one. -* The `FormModel` contains the form state values. -* The `FormController` handles the main form submission logic. -* The `FormSteps` handle their own validation and fire submit/next/previous events. -* Some `FormSteps` modify the model state directly some pass that data to the `FormController` for extra computing. -* The `SubmitForm` has hidden form fields with `FormModel` values and POSTs to the Fundraising Application. - -```mermaid -stateDiagram-v2 - FormModel: Globally available FormModel - FormController --> MultiStepDonationForm - MultiStepDonationForm --> FormStepOne - MultiStepDonationForm --> FormStepTwo - MultiStepDonationForm --> FormStepThree - MultiStepDonationForm --> SubmitForm: Used to post data to Fundraising Application -``` - -Below is an example happy path user flow. - -```mermaid -sequenceDiagram - participant FormController - participant MultiStepDonationForm - participant FormStep1 - participant FormStep2 - participant SubmitForm - - MultiStepDonationForm -->> FormController: add callback to onNext - MultiStepDonationForm -->> FormController: add callback to onPrevious - MultiStepDonationForm -->> FormController: add callback to onGoToStep - MultiStepDonationForm -->> FormController: add callback to onSubmit - - FormStep1 ->> MultiStepDonationForm: user submits step 1 - MultiStepDonationForm ->> FormController: pass submit to controller - FormController ->> MultiStepDonationForm: calls onNext callback - MultiStepDonationForm ->> FormStep2: shows next step - - FormStep2 ->> MultiStepDonationForm: user submits step 2 - MultiStepDonationForm ->> FormController: pass submit to controller - FormController ->> MultiStepDonationForm: calls onSubmit callback - MultiStepDonationForm ->> SubmitForm: posts form -``` - -## Notes for the future - -This module isn't perfect. Depending on the direction of the future campaign tests we should improve some things about it. - -* The form user flows are badly designed. For example, we don't know the reasoning why some forms clear their own state on back. This needs to be clarified before a redesign happens. -* `FormControllers` These are where we put the smelly code. Event handling is done per sub-form using switch statements to branch the logic. This could be improved somewhat. -* Some of the FormControllers are very similar. We might extract them into shared classes for common sets of form steps. -* `FormSteps` Some of these modify the `FormModel` directly, some pass extra data so the `FormController` can modify this data. These steps could be changed to each contain their own state and the `FormController` becomes responsible for changing all `FormModel` state, meaning the `FormModel` no longer needs to be so global.