Files
notifications-admin/app/assets/javascripts/updateContent.js
Tom Byers 8521d1e45f Add assertions against stray classes
In previous iterations of the classPersister, we
found issues with the implementation meant classes
it should have added back to elements were also
added to other elements. This adds tests for this
scenario to ensure it doesn't happen again.

Also includes changes to fix a linting error with
the JS which complained about a function being
defined in a loop while referencing variables in
the outer scope.
2022-02-18 12:07:41 +00:00

147 lines
4.3 KiB
JavaScript

(function(global) {
"use strict";
var queues = {};
var morphdom = global.GOVUK.vendor.morphdom;
var defaultInterval = 2000;
var interval = 0;
var calculateBackoff = responseTime => parseInt(Math.max(
(250 * Math.sqrt(responseTime)) - 1000,
1000
));
// Methods to ensure the DOM fragment is clean of classes added by JS before diffing
// and that they are replaced afterwards.
//
// Added to allow the use of JS, in main.js, to apply styles which in future could be
// achieved with the :has pseudo-class. If :has is available in our supported browsers,
// this can be removed in favour of a CSS-only solution.
var ClassesPersister = function ($contents) {
this._$contents = $contents;
this._classNames = [];
this._classesTo$ElsMap = {};
};
ClassesPersister.prototype.addClassName = function (className) {
if (this._classNames.indexOf(className) === -1) {
this._classNames.push(className);
}
};
ClassesPersister.prototype.remove = function () {
// Store references to any elements with class names to persist
this._classNames.forEach(className => {
var $elsWithClassName = $('.' + className, this._$contents).removeClass(className);
if ($elsWithClassName.length > 0) {
this._classesTo$ElsMap[className] = $elsWithClassName;
}
});
};
ClassesPersister.prototype.replace = function () {
var replaceClasses = (idx, el) => {
// Avoid updating elements that are no longer present.
// elements removed will still exist in memory but won't be attached to the DOM any more
if (global.document.body.contains(el)) {
$(el).addClass(className);
}
};
var className;
for (className in this._classesTo$ElsMap) {
this._classesTo$ElsMap[className].each(replaceClasses);
}
// remove references to elements
this._classesTo$ElsMap = {};
};
var getRenderer = ($contents, key, classesPersister) => response => {
classesPersister.remove();
morphdom(
$contents.get(0),
$(response[key]).get(0)
);
classesPersister.replace();
};
var getQueue = resource => (
queues[resource] = queues[resource] || []
);
var flushQueue = function(queue, response) {
while(queue.length) queue.shift()(response);
};
var clearQueue = queue => (queue.length = 0);
var poll = function(renderer, resource, queue, form) {
let startTime = Date.now();
if (document.visibilityState !== "hidden" && queue.push(renderer) === 1) $.ajax(
resource,
{
'method': form ? 'post' : 'get',
'data': form ? $('#' + form).serialize() : {}
}
).done(
response => {
flushQueue(queue, response);
if (response.stop === 1) {
poll = function(){};
}
interval = calculateBackoff(Date.now() - startTime);
}
).fail(
() => poll = function(){}
);
setTimeout(
() => poll.apply(window, arguments), interval
);
};
global.GOVUK.Modules.UpdateContent = function() {
this.start = component => {
var $component = $(component);
var $contents = $component.children().eq(0);
var key = $component.data('key');
var resource = $component.data('resource');
var form = $component.data('form');
var classesPersister = new ClassesPersister($contents);
// Replace component with contents.
// The renderer does this anyway when diffing against the first response
$component.replaceWith($contents);
// Store any classes that should persist through updates
//
// Added to allow the use of JS, in main.js, to apply styles which in future could be
// achieved with the :has pseudo-class. If :has is available in our supported browsers,
// this can be removed in favour of a CSS-only solution.
if ($contents.data('classesToPersist') !== undefined) {
$contents.data('classesToPersist')
.split(' ')
.forEach(className => classesPersister.addClassName(className));
}
setTimeout(
() => poll(
getRenderer($contents, key, classesPersister),
resource,
getQueue(resource),
form
),
defaultInterval
);
};
};
global.GOVUK.Modules.UpdateContent.calculateBackoff = calculateBackoff;
})(window);