feat: the newsletter form submits with htmx and reports failed signups

The newsletter subscription plugin now sits in a form island like the
EXT:form forms: hx-boost posts to the page itself and only
#newsletter-island is swapped (validation errors, confirmation), with the
spinner overlay while the request runs. The id keeps htmx away from other
islands on the same page (both newsletter pages have hidden EXT:form
forms). Without JavaScript the form still posts normally.

The island markup moves from Form/Render.html into Partials/Form/Island.html,
shared by both; the form plugin's partial path is now Partials/ for that.
The comment about static pages is gone with it, static rendering is retired.

When the MyE&P API call fails, the result page shows an error with a link
back to the form instead of the thank-you message.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
frommeandClaude Opus 5.5 committed 2026-10-26 09:17:16 +01:00
1 parent 88fac7313e
commit f0b23b37b9
5 files changed
+132 -108

No files matched your search

@@ -4,8 +4,8 @@
plugin.tx_form.view {
templateRootPaths.10 = EXT:ep_theme/Resources/Private/Templates/Form/
# Spinner.html for Render.html
partialRootPaths.10 = EXT:ep_theme/Resources/Private/Partials/Form/
# Form/Island.html for Render.html
partialRootPaths.10 = EXT:ep_theme/Resources/Private/Partials/
}
plugin.tx_form.settings.yamlConfigurations {
@@ -0,0 +1,19 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="form-island"{f:if(condition: id, then: ' id="{id}"')}>
{content -> f:format.raw()}
<div class="form-island__loading" aria-hidden="true">
<f:render partial="Form/Spinner" arguments="{class: 'form-island__spinner'}" />
</div>
<style>
.form-island { position: relative; }
.form-island__loading { position: absolute; inset: 0; z-index: 10; display: flex; align-items: center; justify-content: center; background: rgb(255 255 255 / .7); visibility: hidden; opacity: 0; transition: opacity .15s, visibility .15s; }
.form-island.htmx-request .form-island__loading { visibility: visible; opacity: 1; }
.form-island__spinner { width: 6rem; height: 6rem; color: rgb(24 82 123); }
.sbw .form-island__spinner { color: rgb(233 28 38); }
.snz .form-island__spinner { color: rgb(249 195 61); }
.suz .form-island__spinner { color: rgb(130 195 198); }
.uch .form-island__spinner { color: rgb(25 163 220); }
.ser .form-island__spinner { color: rgb(222 44 27); }
</style>
</div>
</html>
@@ -1,31 +1,10 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" xmlns:formvh="http://typo3.org/ns/TYPO3/CMS/Form/ViewHelpers" data-namespace-typo3-fluid="true">
<f:comment>
Every form sits in a .form-island, on pages TYPO3 13 renders as well as in the form page type
(/path/form) that the static pages load: the form posts there, and htmx swaps the island as a
whole (see Form.html). While a request runs, htmx sets htmx-request on it and the overlay
covers the form. Styled here and not in the theme build: the static pages around the island
load the CSS of the TYPO3 10 export. The spinner takes the button colour of each theme.
</f:comment>
<div class="form-island">
<f:render partial="Form/Island" contentAs="content">
<f:flashMessages />
<f:if condition="{formConfiguration}">
<div class="py-8">
<formvh:render overrideConfiguration="{formConfiguration}" />
</div>
</f:if>
<div class="form-island__loading" aria-hidden="true">
<f:render partial="Spinner" arguments="{class: 'form-island__spinner'}" />
</div>
<style>
.form-island { position: relative; }
.form-island__loading { position: absolute; inset: 0; z-index: 10; display: flex; align-items: center; justify-content: center; background: rgb(255 255 255 / .7); visibility: hidden; opacity: 0; transition: opacity .15s, visibility .15s; }
.form-island.htmx-request .form-island__loading { visibility: visible; opacity: 1; }
.form-island__spinner { width: 6rem; height: 6rem; color: rgb(24 82 123); }
.sbw .form-island__spinner { color: rgb(233 28 38); }
.snz .form-island__spinner { color: rgb(249 195 61); }
.suz .form-island__spinner { color: rgb(130 195 198); }
.uch .form-island__spinner { color: rgb(25 163 220); }
.ser .form-island__spinner { color: rgb(222 44 27); }
</style>
</div>
</f:render>
</html>
@@ -5,82 +5,90 @@
<f:section name="Main">
<a id="newsletter-form" class="anchor"></a>
<div class="container">
<f:form method="post" action="subscriptionFormSubmit" name="subscriptionRequest" section="newsletter-form" object="{subscriptionRequest}">
<div class="grid lg:grid-cols-2 lg:gap-x-8 items-start">
<div>
<f:render section="Field" arguments="{property: 'firstName', label: 'Vorname'}"/>
<f:render section="Field" arguments="{property: 'lastName', label: 'Nachname'}"/>
<f:render section="Field" arguments="{property: 'email', label: 'E-Mail'}"/>
<div class="mb-4">
<f:form.validationResults for="subscriptionRequest.consent">
<label class="inline-flex items-center">
<f:form.checkbox property="consent" value="1" class="form-checkbox{f:if(condition: validationResults.errors.0, then: ' form-checkbox--has-error')}"/>
<span class="block ml-2">Ich akzeptiere die <f:link.typolink parameter="{settings.dataProtectionPageUid}" class="underline" target="_blank">Datenschutzbestimmungen</f:link.typolink>*</span>
</label>
<f:if condition="{validationResults.flattenedErrors}">
<div class="text-red-500 mt-2" role="alert">
<f:for each="{validationResults.errors}" as="error">
{f:translate(key: 'tx_eptheme.message.newsletter.consent.{error.code}', extensionName: 'ep_theme')}
<br />
</f:for>
</div>
</f:if>
</f:form.validationResults>
<f:render partial="Form/Island" arguments="{id: 'newsletter-island'}" contentAs="content">
<div class="container">
<f:form method="post" action="subscriptionFormSubmit" name="subscriptionRequest" section="newsletter-form" object="{subscriptionRequest}"
hx-boost="true"
hx-target="#newsletter-island"
hx-select="#newsletter-island"
hx-swap="outerHTML"
hx-push-url="false"
hx-indicator="#newsletter-island">
<div class="grid lg:grid-cols-2 lg:gap-x-8 items-start">
<div>
<f:render section="Field" arguments="{property: 'firstName', label: 'Vorname'}"/>
<f:render section="Field" arguments="{property: 'lastName', label: 'Nachname'}"/>
<f:render section="Field" arguments="{property: 'email', label: 'E-Mail'}"/>
<div class="mb-4">
<f:form.validationResults for="subscriptionRequest.consent">
<label class="inline-flex items-center">
<f:form.checkbox property="consent" value="1" class="form-checkbox{f:if(condition: validationResults.errors.0, then: ' form-checkbox--has-error')}"/>
<span class="block ml-2">Ich akzeptiere die <f:link.typolink parameter="{settings.dataProtectionPageUid}" class="underline" target="_blank">Datenschutzbestimmungen</f:link.typolink>*</span>
</label>
<f:if condition="{validationResults.flattenedErrors}">
<div class="text-red-500 mt-2" role="alert">
<f:for each="{validationResults.errors}" as="error">
{f:translate(key: 'tx_eptheme.message.newsletter.consent.{error.code}', extensionName: 'ep_theme')}
<br />
</f:for>
</div>
</f:if>
</f:form.validationResults>
</div>
<div class="hidden lg:block">
<button type="submit" class="button bg-button">
Anmelden
</button>
</div>
</div>
<div class="hidden lg:block">
<div>
<div>
<f:form.validationResults for="subscriptionRequest.listIds">
<f:for each="{lists}" as="list" iteration="iteration">
<f:comment><!-- show different headline for first newsletter in list --></f:comment>
<f:if condition="{iteration.isFirst}">
<f:then>
<h3>
News &amp; Angebote per Mail
</h3>
</f:then>
<f:else>
<f:comment><!-- show headline only once --></f:comment>
<f:if condition="{alertHeadline}">
<f:else>
<f:variable name="alertHeadline" value="1"/>
<h3 class="pt-4">
Benachrichtige mich bei Verfügbarkeit
</h3>
</f:else>
</f:if>
</f:else>
</f:if>
<label for="list-{list.id}" class="flex items-center">
<f:form.checkbox property="listIds" multiple="1" value="{list.id}" class="form-checkbox"/>
<span class="block ml-2">{list.label}</span>
</label>
</f:for>
<f:if condition="{validationResults.flattenedErrors}">
<div class="text-red-500 mt-2" role="alert">
<f:for each="{validationResults.errors}" as="error">
{f:translate(key: 'tx_eptheme.message.newsletter.listIds.{error.code}', extensionName: 'ep_theme')}
<br />
</f:for>
</div>
</f:if>
</f:form.validationResults>
</div>
</div>
<div class="lg:hidden mt-4">
<button type="submit" class="button bg-button">
Anmelden
</button>
</div>
</div>
<div>
<div>
<f:form.validationResults for="subscriptionRequest.listIds">
<f:for each="{lists}" as="list" iteration="iteration">
<f:comment><!-- show different headline for first newsletter in list --></f:comment>
<f:if condition="{iteration.isFirst}">
<f:then>
<h3>
News &amp; Angebote per Mail
</h3>
</f:then>
<f:else>
<f:comment><!-- show headline only once --></f:comment>
<f:if condition="{alertHeadline}">
<f:else>
<f:variable name="alertHeadline" value="1"/>
<h3 class="pt-4">
Benachrichtige mich bei Verfügbarkeit
</h3>
</f:else>
</f:if>
</f:else>
</f:if>
<label for="list-{list.id}" class="flex items-center">
<f:form.checkbox property="listIds" multiple="1" value="{list.id}" class="form-checkbox"/>
<span class="block ml-2">{list.label}</span>
</label>
</f:for>
<f:if condition="{validationResults.flattenedErrors}">
<div class="text-red-500 mt-2" role="alert">
<f:for each="{validationResults.errors}" as="error">
{f:translate(key: 'tx_eptheme.message.newsletter.listIds.{error.code}', extensionName: 'ep_theme')}
<br />
</f:for>
</div>
</f:if>
</f:form.validationResults>
</div>
</div>
<div class="lg:hidden mt-4">
<button type="submit" class="button bg-button">
Anmelden
</button>
</div>
</div>
</f:form>
</div>
</f:form>
</div>
</f:render>
</f:section>
<f:section name="Field">
@@ -4,20 +4,38 @@
<f:layout name="Default"/>
<f:section name="Main">
<div class="container">
<div class="bg-ep-primary-light text-white p-4">
<h2>
Vielen Dank für dein Interesse
</h2>
<p>
Deine Anmeldung muss noch bestätigt werden. Bitte nutze den Link aus der Bestätigungs-E-Mail, die in
Kürze bei dir eintreffen sollte.
</p>
<p class="text-sm">
Sieh am besten auch im Spam-Ordner nach.
</p>
<a id="newsletter-form" class="anchor"></a>
<f:render partial="Form/Island" arguments="{id: 'newsletter-island'}" contentAs="content">
<div class="container">
<f:if condition="{result.success}">
<f:then>
<div class="bg-ep-primary-light text-white p-4">
<h2>
Vielen Dank für dein Interesse
</h2>
<p>
Deine Anmeldung muss noch bestätigt werden. Bitte nutze den Link aus der Bestätigungs-E-Mail, die in
Kürze bei dir eintreffen sollte.
</p>
<p class="text-sm">
Sieh am besten auch im Spam-Ordner nach.
</p>
</div>
</f:then>
<f:else>
<div class="bg-red-600 text-white p-4" role="alert">
<h2>
Das hat leider nicht geklappt
</h2>
<p>
Deine Anmeldung konnte gerade nicht gespeichert werden. Bitte versuche es in ein paar Minuten
<f:link.page section="newsletter-form" absolute="1" class="underline">noch einmal</f:link.page>.
</p>
</div>
</f:else>
</f:if>
</div>
</div>
</f:render>
</f:section>
</html>