Patch update sections of the page on AJAX

Currently, when we update a section of the page with AJAX we replace the
entire HTML of the section with the new HTML. This causes problems:
- if you’re trying to interact with that section of the page, eg by
  inpecting it, clicking or hovering an element
- (probably) for screenreaders trying to navigate a page which is
  changing more than is necessary

This commit replaces the call to `.html()` with a pretty clever library
called diffDOM[1]. DiffDOM works by taking a diff of the old element and
the new element, then doing a patch update, ie only modifying the parts
that have changed.

This is similar in concept to React’s virtual DOM, while still allowing
us to render all markup from one set of templates on the server-side.

1. https://github.com/fiduswriter/diffDOM
This commit is contained in:
Chris Hill-Scott
2016-04-27 09:28:42 +01:00
parent 36d2889a59
commit 33b4138e79
9 changed files with 124 additions and 114 deletions

View File

@@ -4,6 +4,8 @@
GOVUK.timeCache = {};
GOVUK.resultCache = {};
var dd = new diffDOM();
let getter = function(resource, interval, render) {
if (
@@ -21,8 +23,11 @@
};
let poller = (resource, key, component, interval) => () => getter(
resource, interval, response => component.html(response[key])
let poller = (resource, key, $component, interval) => () => getter(
resource, interval, response => dd.apply(
$component.get(0),
dd.diff($component.get(0), $(response[key]).get(0))
)
);
Modules.UpdateContent = function() {

View File

@@ -1,13 +1,24 @@
{% from "components/big-number.html" import big_number %}
<ul class="grid-row job-totals">
<li class="column-one-quarter">
{{ big_number(
counts.queued, 'queued'
)}}
</li>
<li class="column-one-quarter">
{{ big_number(
counts.sent, 'processed'
)}}
</li>
</ul>
<div
{% if not finished_at %}
data-module="update-content"
data-resource="{{url_for(".view_job_updates", service_id=current_service.id, job_id=job_id)}}"
data-key="counts"
{% endif %}
>
<ul class="grid-row job-totals">
<li class="column-one-quarter">
{{ big_number(
counts.queued, 'queued'
)}}
</li>
<li class="column-one-quarter">
{{ big_number(
counts.sent, 'processed'
)}}
</li>
</ul>
</div>

View File

@@ -1,22 +1,34 @@
{% from "components/table.html" import list_table, field, right_aligned_field_heading %}
{% call(item, row_number) list_table(
notifications,
caption=uploaded_file_name,
caption_visible=False,
empty_message="No messages to show yet",
field_headings=[
'Recipient',
right_aligned_field_heading('Status')
]
) %}
{% call field() %}
{{ item.to }}
{% endcall %}
{% call field(
align='right',
status='error' if item.status == 'Failed' else 'default'
) %}
{{ item.status|title }} at {{ item.sent_at|format_time }}
{% endcall %}
{% endcall %}
<div
{% if not finished_at %}
data-module="update-content"
data-resource="{{url_for(".view_job_updates", service_id=current_service.id, job_id=job_id)}}"
data-key="notifications"
{% endif %}
>
<div>
{% call(item, row_number) list_table(
notifications,
caption=uploaded_file_name,
caption_visible=False,
empty_message="No messages to show yet",
field_headings=[
'Recipient',
right_aligned_field_heading('Status')
]
) %}
{% call field() %}
{{ item.to }}
{% endcall %}
{% call field(
align='right',
status='error' if item.status == 'Failed' else 'default'
) %}
{{ item.status|title }} at {{ item.sent_at|format_time }}
{% endcall %}
{% endcall %}
</div>
</div>

View File

@@ -1,9 +1,19 @@
{% if finished_at %}
<p class='heading-small'>
Finished {{ finished_at|format_datetime }}
</p>
{% else %}
<p class='heading-small'>
Started {{ uploaded_at|format_datetime }}
</p>
{% endif %}
<div
{% if not finished_at %}
data-module="update-content"
data-resource="{{url_for(".view_job_updates", service_id=current_service.id, job_id=job_id)}}"
data-key="status"
{% endif %}
>
{% if finished_at %}
<p class='heading-small'>
Finished {{ finished_at|format_datetime }}
</p>
{% else %}
<p class='heading-small'>
Started {{ uploaded_at|format_datetime }}
</p>
{% endif %}
</div>

View File

@@ -18,13 +18,6 @@
{% include 'views/dashboard/no-permissions-banner.html' %}
{% endif %}
<div
data-module="update-content"
data-resource="{{url_for(".service_dashboard_updates", service_id=current_service.id)}}"
data-key="today"
data-interval-seconds="10"
>
{% include 'views/dashboard/today.html' %}
</div>
{% include 'views/dashboard/today.html' %}
{% endblock %}

View File

@@ -1,33 +1,40 @@
{% from "components/big-number.html" import big_number_with_status %}
<h2 class="heading-medium">
In the last 7 days
</h2>
<div class="grid-row bottom-gutter">
<div class="column-half">
{{ big_number_with_status(
statistics.get('emails_delivered', 0),
'email' if statistics.get('emails_delivered') == 1 else 'emails',
statistics.get('emails_failed'),
statistics.get('emails_failure_rate', 0.0),
statistics.get('emails_failure_rate', 0)|float > 3,
failure_link=url_for(".view_notifications", service_id=current_service.id, template_type='email', status='failed')
) }}
</div>
<div class="column-half">
{{ big_number_with_status(
statistics.get('sms_requested', 0),
'text message' if statistics.get('sms_requested') == 1 else 'text messages',
statistics.get('sms_failed'),
statistics.get('sms_failure_rate', 0.0),
statistics.get('sms_failure_rate', 0)|float > 3,
failure_link=url_for(".view_notifications", service_id=current_service.id, template_type='sms', status='failed')
) }}
<div
data-module="update-content"
data-resource="{{url_for(".service_dashboard_updates", service_id=current_service.id)}}"
data-key="today"
data-interval-seconds="2"
>
<h2 class="heading-medium">
In the last 7 days
</h2>
<div class="grid-row bottom-gutter">
<div class="column-half">
{{ big_number_with_status(
statistics.get('emails_delivered', 0),
'email' if statistics.get('emails_delivered') == 1 else 'emails',
statistics.get('emails_failed'),
statistics.get('emails_failure_rate', 0.0),
statistics.get('emails_failure_rate', 0)|float > 3,
failure_link=url_for(".view_notifications", service_id=current_service.id, template_type='email', status='failed')
) }}
</div>
<div class="column-half">
{{ big_number_with_status(
statistics.get('sms_requested', 0),
'text message' if statistics.get('sms_requested') == 1 else 'text messages',
statistics.get('sms_failed'),
statistics.get('sms_failure_rate', 0.0),
statistics.get('sms_failure_rate', 0)|float > 3,
failure_link=url_for(".view_notifications", service_id=current_service.id, template_type='sms', status='failed')
) }}
</div>
</div>
{% with period = "in the last 7 days" %}
{% include 'views/dashboard/template-statistics.html' %}
{% endwith %}
<p class='table-show-more-link'>
<a href="{{ url_for('.template_history', service_id=current_service.id) }}">See all templates used this year</a>
</p>
</div>
{% with period = "in the last 7 days" %}
{% include 'views/dashboard/template-statistics.html' %}
{% endwith %}
<p class='table-show-more-link'>
<a href="{{ url_for('.template_history', service_id=current_service.id) }}">See all templates used this year</a>
</p>

View File

@@ -30,40 +30,10 @@
)}}
{% endif %}
{% if not finished_at %}
<div
data-module="update-content"
data-resource="{{url_for(".view_job_updates", service_id=current_service.id, job_id=job_id)}}"
data-key="status"
>
{% endif %}
{% include 'partials/jobs/status.html' %}
{% if not finished_at %}
</div>
{% endif %}
{% include 'partials/jobs/status.html' %}
{% if not finished_at %}
<div
data-module="update-content"
data-resource="{{url_for(".view_job_updates", service_id=current_service.id, job_id=job_id)}}"
data-key="counts"
>
{% endif %}
{% include 'partials/jobs/count.html' %}
{% if not finished_at %}
</div>
{% endif %}
{% include 'partials/jobs/count.html' %}
{% if not finished_at %}
<div
data-module="update-content"
data-resource="{{url_for(".view_job_updates", service_id=current_service.id, job_id=job_id)}}"
data-key="notifications"
>
{% endif %}
{% include 'partials/jobs/notifications.html' %}
{% if not finished_at %}
</div>
{% endif %}
{% include 'partials/jobs/notifications.html' %}
{% endblock %}

View File

@@ -68,7 +68,8 @@ gulp.task('javascripts', () => gulp
.pipe(plugins.uglify())
.pipe(plugins.addSrc.prepend([
paths.npm + 'jquery/dist/jquery.min.js',
paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js'
paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js',
paths.npm + 'diff-dom/diffDOM.js'
]))
.pipe(plugins.concat('all.js'))
.pipe(gulp.dest(paths.dist + 'javascripts/'))

View File

@@ -20,6 +20,7 @@
"dependencies": {
"babel-core": "6.3.26",
"babel-preset-es2015": "6.3.13",
"diff-dom": "2.0.3",
"govuk-elements-sass": "1.1.1",
"govuk_frontend_toolkit": "4.6.0",
"govuk_template_jinja": "https://github.com/alphagov/govuk_template/releases/download/v0.17.1/jinja_govuk_template-0.17.1.tgz",