ws-element/static/index.js

219 lines
6.6 KiB
JavaScript
Raw Normal View History

2023-10-18 16:51:32 -04:00
class WSElement extends HTMLElement {
static #sockets = {};
2023-10-19 14:51:58 -04:00
#socket;
2023-10-18 16:51:32 -04:00
#delay = 1000;
constructor() {
super();
}
connectedCallback() {
// track `src` and `autoreconnect` attributes for later use
// `autoreconnect` defaults to `true`, unless explicitly set to `false`
this.src = this.getAttribute('src');
this.autoreconnect = !this.hasAttribute('autoreconnect') || this.getAttribute('autoreconnect').toLowerCase() !== 'false';
this.#connect_socket(false);
}
#connect_socket(recreate) {
// `recreate` is used to handle reconnects, removeing the old websocket object
// and recreating a new one
if(recreate && WSElement.#sockets[this.src]) {
delete WSElement.#sockets[this.src];
}
// a single WebSocket can be reused for multiple elements
// check to see if one exists for this `src` else create
// a new one.
if(!(this.src in WSElement.#sockets)) {
2023-10-19 14:51:58 -04:00
this.#socket = WSElement.#newSocket(this.src);
WSElement.#sockets[this.src] = this.#socket;
2023-10-18 16:51:32 -04:00
} else {
2023-10-19 14:51:58 -04:00
this.#socket = WSElement.#sockets[this.src];
2023-10-18 16:51:32 -04:00
}
// setup event handlers for this WebSocket/Elemnent pair
2023-10-19 14:51:58 -04:00
this.#socket.addEventListener('error', this.#on_ws_error.bind(this));
this.#socket.addEventListener('message', this.#on_ws_message.bind(this));
this.#socket.addEventListener('open', this.#on_ws_open.bind(this));
this.#socket.addEventListener('close', this.#on_ws_close.bind(this));
2023-10-18 16:51:32 -04:00
}
#on_ws_open(sock) {
// set connected attribute for use in CSS
// ```
// ws-element[connected] {
// color: green;
// }
// ws-element:not(connected) {
// color: red;
// }
// ```
this.setAttribute('connected', '');
// call this.open if defined by subclass
if('open' in this) {
this.open(sock);
}
}
#on_ws_message(sock) {
// call this.message, which is always defined
this.message(sock.data);
}
#on_ws_close(sock) {
this.removeAttribute('connected');
// clean up event handlers for this element
sock.target.removeEventListener('error', this.#on_ws_error.bind(this));
sock.target.removeEventListener('message', this.#on_ws_message.bind(this));
sock.target.removeEventListener('open', this.#on_ws_open.bind(this));
sock.target.removeEventListener('close', this.#on_ws_close.bind(this));
// call this.close if subclass has defined it
if('close' in this) {
this.close(sock);
}
// handle autoreconnect and delay throttling
if(this.autoreconnect) {
setTimeout(this.#connect_socket.call(this, true), this.#delay);
if(this.#delay < 10000) {
this.#delay += 1000;
}
}
}
#on_ws_error(sock) {
// call this.error if subclass has defined it
if('error' in this) {
this.error(sock);
}
}
// default behavior on this.message is just update innerText
message(data) {
this.innerText = data;
}
// static method for creating new sockets, which are tracked
// on the static propert WSElement.#sockets
static #newSocket(src) {
var s = window.location.protocol.slice(-2) === 'p:' ? '' : 's';
var host = window.location.host;
var path = src[0] === '/' ? src.slice(1) : src;
var wsurl = `ws${s}://${host}/${path}`;
return new WebSocket(wsurl);
}
}
// Example subclass to display a basic 'Time: 00:00:00'
class WSTime extends WSElement {
connectedCallback() {
super.connectedCallback();
// insert subelements for the label (Time:) and the value;
this.insertAdjacentHTML('beforeend', '<label class="time-label">Time: </label><span class="time-value"></span>');
// track the value element for updating later
this.value = this.querySelector('span.time-value');
}
// override this.message to update the innerText of the value span element
message(data) {
this.value.innerText = data;
}
}
// Example subclass to parse arbitrary json and build a table
// with dynamically controlled limit
class WSTable extends WSElement {
// observe limit to dynamically update
static observedAttributes = [ 'limit' ];
connectedCallback() {
super.connectedCallback();
// ignoreNext is to handle duplicate headers being sent on reconnect
this.ignoreNext = false;
// insert and track table element
this.insertAdjacentHTML('beforeend', '<table class="table"></table>');
this.table = this.querySelector('table');
// track limit attribute, defaulting to no limit
if(this.hasAttribute('limit')) {
this.limit = parseInt(this.getAttribute('limit')) || -1;
}
}
attributeChangedCallback(name, oldValue, newValue) {
// update the tracked limit, defaulting to none if no value provided
if(oldValue === null) {
this.limit = parseInt(newValue) || -1;
}
}
message(data) {
// ignore one message, because the demo server is a bit dumb
if(this.ignoreNext) {
this.ignoreNext = false;
return;
}
data = JSON.parse(data);
// with autoheader set the first message will create a header row instead of body
if(this.hasAttribute('autoheader') && !('header' in this)) {
// createElement works just as well as insertAdjacentHTML for single, to be tracked elements
this.thead = document.createElement('thead');
this.header = document.createElement('tr');
this.thead.appendChild(this.header);
this.table.appendChild(this.thead);
// add a `th` for each key/value pair in the json data
for(let k in data) {
this.header.insertAdjacentHTML('beforeend', `<th scope="col" class="${k}">${data[k]}</th>`);
}
// work is done, this block will be ignored in future messages
return;
}
// create the body if it doesn't yet exists
if(!('tbody' in this)) {
this.tbody = document.createElement('tbody');
this.table.appendChild(this.tbody);
}
// create TD for each key/value pair in message data
var inner = '';
for(let k in data) {
inner += `<td class="${k}">${data[k]}</td>`;
}
// instead the above TDs into a new TR
this.tbody.insertAdjacentHTML('beforeend', `<tr>${inner}</tr>`);
// check limits are enforced
if(this.limit > -1 && this.tbody.childElementCount > this.limit) {
while(this.tbody.childElementCount > this.limit) {
this.tbody.removeChild(this.tbody.childNodes[0]);
}
}
}
// ignore the next message if we're disconnected,
// demo server always sends headers on connect
close(sock) {
this.ignoreNext = true;
}
}
// define custom elements
customElements.define('ws-element', WSElement);
customElements.define('ws-time', WSTime);
customElements.define('ws-table', WSTable);