Skip to content

Commit

Permalink
Merge pull request #2443 from alphagov/js-enhancement-single-page-not…
Browse files Browse the repository at this point in the history
…if-button

Get single page notification button from personalisation API on load
  • Loading branch information
danacotoran authored Nov 18, 2021
2 parents 94c0fc7 + 674540b commit 695e7c6
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
useful summary for people upgrading their application, not a replication
of the commit log.

## Unreleased

* Get single page notification button from personalisation API on load ([PR #2443](https://github.com/alphagov/govuk_publishing_components/pull/2443))

## 27.13.0

* Add brand colour for Department for Levelling Up, Housing and Communities (DLUHC) ([PR #2454](https://github.com/alphagov/govuk_publishing_components/pull/2454))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* global XMLHttpRequest */
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};

(function (Modules) {
function SinglePageNotificationButton ($module) {
this.$module = $module
this.basePath = this.$module.querySelector('input[name="base_path"]').value
this.buttonLocation = this.$module.getAttribute('data-button-location')

this.personalisationEndpoint = '/api/personalisation/check-email-subscription?base_path=' + this.basePath
// This attribute is passed through to the personalisation API to ensure the updated button has the same button_location for analytics
if (this.buttonLocation) this.personalisationEndpoint += '&button_location=' + this.buttonLocation
}

SinglePageNotificationButton.prototype.init = function () {
var xhr = new XMLHttpRequest()
xhr.open('GET', this.personalisationEndpoint, true)

xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var responseText = xhr.responseText
// if response text exists and is JSON parse-able, parse the response and get the button html
if (responseText && this.responseIsJSON(responseText)) {
var newButton = JSON.parse(responseText).button_html
var html = document.createElement('div')
html.innerHTML = newButton
// test that the html returned contains the button component; if yes, swap the button for the updated version
var responseHasButton = html.querySelector('form.gem-c-single-page-notification-button .gem-c-single-page-notification-button__submit')
if (responseHasButton) {
this.$module.outerHTML = newButton
}
}
}
}.bind(this)
xhr.send()
}

SinglePageNotificationButton.prototype.responseIsJSON = function (string) {
try {
JSON.parse(string)
} catch (e) {
return false
}
return true
}
Modules.SinglePageNotificationButton = SinglePageNotificationButton
})(window.GOVUK.Modules)
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
<%
page ||= ''
data_attributes ||= {}
base_path ||= nil
local_assigns[:margin_bottom] ||= 3
already_subscribed ||= false
text ||= already_subscribed ? t('components.single_page_notification_button.unsubscribe_text') : t('components.single_page_notification_button.subscribe_text')

component_helper = GovukPublishingComponents::Presenters::SinglePageNotificationButtonHelper.new(local_assigns)
shared_helper = GovukPublishingComponents::Presenters::SharedHelper.new(local_assigns)

wrapper_classes = %w(gem-c-single-page-notification-button govuk-!-display-none-print)
wrapper_classes << shared_helper.get_margin_bottom
classes = "govuk-body-s gem-c-single-page-notification-button__submit"
%>
<% button_text = capture do %>
<svg class="gem-c-single-page-notification-button__icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 459.334 459.334"><path fill="currentColor" d="M177.216 404.514c-.001.12-.009.239-.009.359 0 30.078 24.383 54.461 54.461 54.461s54.461-24.383 54.461-54.461c0-.12-.008-.239-.009-.359H175.216zM403.549 336.438l-49.015-72.002v-89.83c0-60.581-43.144-111.079-100.381-122.459V24.485C254.152 10.963 243.19 0 229.667 0s-24.485 10.963-24.485 24.485v27.663c-57.237 11.381-100.381 61.879-100.381 122.459v89.83l-49.015 72.002a24.76 24.76 0 0 0 20.468 38.693H383.08a24.761 24.761 0 0 0 20.469-38.694z"/></svg><%= text %>
<svg class="gem-c-single-page-notification-button__icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 459.334 459.334"><path fill="currentColor" d="M177.216 404.514c-.001.12-.009.239-.009.359 0 30.078 24.383 54.461 54.461 54.461s54.461-24.383 54.461-54.461c0-.12-.008-.239-.009-.359H175.216zM403.549 336.438l-49.015-72.002v-89.83c0-60.581-43.144-111.079-100.381-122.459V24.485C254.152 10.963 243.19 0 229.667 0s-24.485 10.963-24.485 24.485v27.663c-57.237 11.381-100.381 61.879-100.381 122.459v89.83l-49.015 72.002a24.76 24.76 0 0 0 20.468 38.693H383.08a24.761 24.761 0 0 0 20.469-38.694z"/></svg><%= component_helper.button_text %>
<% end %>
<%= tag.form class: wrapper_classes, action: "/email/subscriptions/single-page/new", method: "POST", data: data_attributes do %>
<input type="hidden" name="base_path" value="<%= base_path %>">
<%= tag.form class: wrapper_classes, action: "/email/subscriptions/single-page/new", method: "POST", data: component_helper.data do %>
<input type="hidden" name="base_path" value="<%= component_helper.base_path %>">
<%= content_tag(:button, button_text, {
class: classes,
class: "govuk-body-s gem-c-single-page-notification-button__submit",
type: "submit",
}) %>
<% end if base_path.presence %>
<% end if component_helper.base_path %>
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
name: Single page notification button
description: A button that subscribes the user to email notifications to a page
body: |
By default, the component displays with the "Get emails about this page" state. The component does not render without the `base_path` parameter.
By default, the component displays with the "Get emails about this page" state.
If the `js-enhancement` flag is present, the component uses JavaScript to check if the user has already subscribed to email notifications on the current page. If yes, the state of the component updates accordingly.
The `base_path` is necessary for [checking if an email subscription is active on page load](https://github.com/alphagov/account-api/blob/main/docs/api.md#get-apipersonalisationcheck-email-subscriptiontopic_slug) and the creation/deletion of an email notification subscription.
The component does not render without the `base_path` parameter. The `base_path` is necessary for [checking if an email subscription is active on page load](https://github.com/alphagov/account-api/blob/main/docs/api.md#get-apipersonalisationcheck-email-subscriptiontopic_slug) and the creation/deletion of an email notification subscription.
When the button is clicked, the `base_path` is submitted to an endpoint which proceeds to check the user's authentication status and whether they are already subscribed to the page or not. Depending on these factors, they will be routed accordingly.
accessibility_criteria: |
Expand All @@ -23,10 +24,27 @@ examples:
data:
base_path: '/current-page-path'
data_attributes:
category: fancyButtons
test_attribute: "testing"
with_margin_bottom:
description: |
The component accepts a number for margin bottom from 0 to 9 (0px to 60px) using the [GOV.UK Frontend spacing scale](https://design-system.service.gov.uk/styles/spacing/#the-responsive-spacing-scale). It defaults to having a margin bottom of 15px.
data:
base_path: '/current-page-path'
margin_bottom: 5
with_js_enhancement:
description: |
If the `js-enhancement` flag is present, the component uses JavaScript to check if the user has already subscribed to email notifications on the current page. If yes, the state of the component updates accordingly.
data:
base_path: '/current-page-path'
js_enhancement: true
with_button_location:
description: |
When there is more than one button on a page, we should specify their location so that Analytics can differentiate between them.
The location should have one of two values: "top" or "bottom".
When this parameter is passed, its value is reflected in the `data-action` attribute (i.e "Unsubscribe-button-top"). When the flag is not present, `data-action` defaults to "Subscribe-button" or "Unsubscribe-button", depending on the state of the button.
data:
base_path: '/current-page-path'
js_enhancement: true
button_location: 'top'
1 change: 1 addition & 0 deletions lib/govuk_publishing_components.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
require "govuk_publishing_components/presenters/organisation_logo_helper"
require "govuk_publishing_components/presenters/highlight_boxes_helper"
require "govuk_publishing_components/presenters/taxonomy_list_helper"
require "govuk_publishing_components/presenters/single_page_notification_button_helper"

require "govuk_publishing_components/app_helpers/taxon_breadcrumbs"
require "govuk_publishing_components/app_helpers/table_helper"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module GovukPublishingComponents
module Presenters
class SinglePageNotificationButtonHelper
attr_reader :already_subscribed, :data_attributes, :base_path, :js_enhancement, :button_type, :button_location

def initialize(local_assigns)
@local_assigns = local_assigns
@data_attributes = @local_assigns[:data_attributes] || {}
@js_enhancement = @local_assigns[:js_enhancement] || false
@already_subscribed = @local_assigns[:already_subscribed] || false
@base_path = @local_assigns[:base_path] || nil
@button_location = button_location_is_valid? ? @local_assigns[:button_location] : nil
@button_type = @local_assigns[:already_subscribed] ? "Unsubscribe" : "Subscribe"
end

def data
module_names = %w[gem-track-click]
module_names << "single-page-notification-button" if js_enhancement

@data_attributes[:label] = base_path
# data-action for tracking should have the format of e.g. "Unsubscribe-button-top", or "Subscribe-button-bottom"
# when button_location is not present data-action will fall back to "Unsubscribe-button"/"Subscribe-button"
@data_attributes[:action] = [button_type, "button", button_location].compact.join("-")
@data_attributes[:module] = module_names.join(" ")
@data_attributes[:category] = "Single-page-notification-button"
# This attribute is passed through to the personalisation API to ensure when a new button is returned from the API, it has the same button_location
@data_attributes[:button_location] = button_location
@data_attributes
end

def button_location_is_valid?
%w[bottom top].include? @local_assigns[:button_location]
end

def button_text
@already_subscribed ? I18n.t("components.single_page_notification_button.unsubscribe_text") : I18n.t("components.single_page_notification_button.subscribe_text")
end
end
end
end
53 changes: 51 additions & 2 deletions spec/components/single_page_notification_button_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def component_name
end

it "has data attributes if data_attributes is specified" do
render_component({ base_path: "/the-current-page", data_attributes: { action: "kaboom!" } })
assert_select ".gem-c-single-page-notification-button[data-action='kaboom!']"
render_component({ base_path: "/the-current-page", data_attributes: { custom_attribute: "kaboom!" } })
assert_select ".gem-c-single-page-notification-button[data-custom-attribute='kaboom!']"
end

it "sets a default bottom margin" do
Expand All @@ -40,4 +40,53 @@ def component_name
render_component({ base_path: "/the-current-page", margin_bottom: 9 })
assert_select '.gem-c-single-page-notification-button.govuk-\!-margin-bottom-9'
end

it "has a data-module attribute for JavaScript, if the js-enhancement flag is present" do
render_component({ base_path: "/the-current-page", js_enhancement: true })
dom = Nokogiri::HTML(rendered)
form_data_module = dom.xpath("//form")[0].attr("data-module")

expect(form_data_module).to include("single-page-notification-button")
end

it "has correct attributes for tracking by default" do
render_component({ base_path: "/the-current-page" })
assert_select ".gem-c-single-page-notification-button[data-category='Single-page-notification-button'][data-action='Subscribe-button'][data-label='/the-current-page']"
end

it "has correct attributes for tracking when already_subscribed is true" do
render_component({ base_path: "/the-current-page", already_subscribed: true })

assert_select ".gem-c-single-page-notification-button[data-category='Single-page-notification-button'][data-action='Unsubscribe-button'][data-label='/the-current-page']"
end

it "has the correct default data-action for tracking when button_location is top" do
render_component({ base_path: "/the-current-page", button_location: "top" })

assert_select ".gem-c-single-page-notification-button[data-action='Subscribe-button-top']"
end

it "has the correct data-action for tracking when button_location is top and already_subscribed is true" do
render_component({ base_path: "/the-current-page", button_location: "top", already_subscribed: true })

assert_select ".gem-c-single-page-notification-button[data-action='Unsubscribe-button-top']"
end

it "has the correct default data-action for tracking when button_location is bottom" do
render_component({ base_path: "/the-current-page", button_location: "bottom" })

assert_select ".gem-c-single-page-notification-button[data-action='Subscribe-button-bottom']"
end

it "has the correct data-action for tracking when button_location is bottom and already_subscribed is true" do
render_component({ base_path: "/the-current-page", button_location: "bottom", already_subscribed: true })

assert_select ".gem-c-single-page-notification-button[data-action='Unsubscribe-button-bottom']"
end

it "has the correct data-action for tracking when button_location has an invalid value" do
render_component({ base_path: "/the-current-page", button_location: "this is unacceptable" })

assert_select ".gem-c-single-page-notification-button[data-action='Subscribe-button']"
end
end
2 changes: 2 additions & 0 deletions spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@
get "error-summary", to: "welcome#errorsummary"
get "tabsexample", to: "welcome#tabsexample"
get "table", to: "welcome#table"
# we fake this URL to prevent the Single Page notification button from causing an error in the component guide
get "/api/personalisation/check-email-subscription", to: proc { [404, {}, [""]] }
end
102 changes: 102 additions & 0 deletions spec/javascripts/components/single-page-notification-button-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* eslint-env jasmine */
/* global GOVUK */

describe('Single page notification component', function () {
var FIXTURE

beforeEach(function () {
FIXTURE =
'<form class="gem-c-single-page-notification-button old-button-for-test" action="/email/subscriptions/single-page/new" method="POST" data-module="single-page-notification-button">' +
'<input type="hidden" name="base_path" value="/current-page-path">' +
'<button class="gem-c-single-page-notification-button__submit" type="submit">Get emails about this page</button>' +
'</form>'
window.setFixtures(FIXTURE)
jasmine.Ajax.install()
})

afterEach(function () {
jasmine.Ajax.uninstall()
})

it('calls the personalisation API on load', function () {
initButton()
var request = jasmine.Ajax.requests.mostRecent()
expect(request.url).toBe('/api/personalisation/check-email-subscription?base_path=/current-page-path')
expect(request.method).toBe('GET')
})

it('includes button_location in the call to the personalisation API when button_location is specified', function () {
FIXTURE =
'<form class="gem-c-single-page-notification-button old-button-for-test" action="/email/subscriptions/single-page/new" method="POST" data-module="single-page-notification-button" data-button-location="top">' +
'<input type="hidden" name="base_path" value="/current-page-path">' +
'<button class="gem-c-single-page-notification-button__submit" type="submit">Get emails about this page</button>' +
'</form>'
window.setFixtures(FIXTURE)

initButton()
var request = jasmine.Ajax.requests.mostRecent()
expect(request.url).toBe('/api/personalisation/check-email-subscription?base_path=/current-page-path&button_location=top')
expect(request.method).toBe('GET')
})

it('replaces the button when the API returns button html', function () {
initButton()

jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: '{\n "base_path": "/current-page-path",\n "active": true,\n "button_html": "<form class=\\"gem-c-single-page-notification-button new-button-for-test\\" action=\\"/email/subscriptions/single-page/new\\" method=\\"POST\\">\\n <input type=\\"hidden\\" name=\\"base_path\\" value=\\"/current-page-path\\">\\n <button class=\\"gem-c-single-page-notification-button__submit\\" type=\\"submit\\">Stop getting emails about this page\\n</button>\\n</form>"\n}'
})

var button = document.querySelector('form.gem-c-single-page-notification-button.new-button-for-test button')
expect(button).toHaveText('Stop getting emails about this page')
})

it('should remain unchanged if the response is not JSON', function () {
var responseText = 'I am not JSON, actually'
initButton()

jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: responseText
})

var button = document.querySelector('form.gem-c-single-page-notification-button.old-button-for-test button')
expect(button).toHaveText('Get emails about this page')
expect(GOVUK.Modules.SinglePageNotificationButton.prototype.responseIsJSON(responseText)).toBe(false)
})

it('should remain unchanged if response text is empty', function () {
var responseText = ''
initButton()

jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: responseText
})

var button = document.querySelector('form.gem-c-single-page-notification-button.old-button-for-test button')
expect(button).toHaveText('Get emails about this page')
expect(GOVUK.Modules.SinglePageNotificationButton.prototype.responseIsJSON(responseText)).toBe(false)
})

it('should remain unchanged if the endpoint fails', function () {
initButton()

jasmine.Ajax.requests.mostRecent().respondWith({
status: 500,
contentType: 'text/plain',
responseText: ''
})

var button = document.querySelector('form.gem-c-single-page-notification-button.old-button-for-test button')
expect(button).toHaveText('Get emails about this page')
})

function initButton () {
var element = document.querySelector('[data-module=single-page-notification-button]')
new GOVUK.Modules.SinglePageNotificationButton(element).init()
}
})

0 comments on commit 695e7c6

Please sign in to comment.