Skip to content

Commit

Permalink
load partial pages (#11)
Browse files Browse the repository at this point in the history
- boost links so we don't load full page
- implement "routing"
- ensure setup event happens before load eventload partial pages
  • Loading branch information
chungg authored Dec 21, 2023
1 parent a887217 commit d4181e3
Show file tree
Hide file tree
Showing 15 changed files with 256 additions and 219 deletions.
1 change: 1 addition & 0 deletions app/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@


from app.api import root # noqa
from app.api import views # noqa


bp.register_blueprint(bp_v1, url_prefix='/api/v1')
16 changes: 0 additions & 16 deletions app/api/root.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,10 @@
import flask
from flask_security import logout_user
import sqlalchemy as sa

from app.api import bp
from app.storage.db import db


@bp.get('/')
def index():
return flask.render_template("index.html")


@bp.get('/analytics')
def analytics():
return flask.render_template("analytics.html")


@bp.get('/yahoo')
def yahoo():
return flask.render_template("yahoo.html")


@bp.get('/logout')
def logout():
logout_user()
Expand Down
18 changes: 18 additions & 0 deletions app/api/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import functools
import json

import flask


def hx_page(template):
"""set response header to trigger event to initialise frontend assets (if needed)"""
def decorator(f):
@functools.wraps(f)
def decorated_function(*args, **kwargs):
template_name = template
ctx = f(*args, **kwargs)
resp = flask.Response(flask.render_template(template_name))
resp.headers['HX-Trigger-After-Settle'] = json.dumps({'initPage': ctx})
return resp
return decorated_function
return decorator
20 changes: 20 additions & 0 deletions app/api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from app.api import bp
from app.api import utils


@bp.get('/')
@utils.hx_page('index.html')
def index():
return {'path': '/'}


@bp.get('/analytics')
@utils.hx_page('analytics.html')
def analytics():
return {'path': '/analytics'}


@bp.get('/yahoo')
@utils.hx_page('yahoo.html')
def yahoo():
return {'path': '/yahoo'}
43 changes: 43 additions & 0 deletions app/static/js/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
function updateChart(evt) {
const chart = Chart.getChart(evt.detail.target);
// https://www.reddit.com/r/htmx/comments/10sdk43/comment/j72m2j7/
const data = JSON.parse(document.getElementById(evt.detail.dataId).textContent);
for (const dataset of data.datasets) {
chart.data.datasets.push(dataset);
}
if (data.labels != null) {
chart.data.labels = data.labels;
}
chart.update();
document.getElementById(evt.detail.dataId).remove();
}

function displayPrices(evt) {
const chart = Chart.getChart(evt.detail.target);
const table = Tabulator.findTable("#priceTable")[0];
const data = JSON.parse(document.getElementById(evt.detail.dataId).textContent);
const prices = data.indicators.quote[0].close;
// ideally, should check overlap of labels between datasets
table.updateOrAddData(data.timestamp.map((a, i) => ({ date: a, [data.meta.symbol]: prices[i] })));
if (!table.columnManager.columns.length) {
table.addColumn({ title: "Date", field: "date", frozen: true }, true);
}
table.addColumn({
title: data.meta.symbol,
field: data.meta.symbol,
hozAlign: "right",
headerSort: false,
formatter: "money",
formatterParams: { thousand: false },
});
chart.data.labels = data.timestamp;
// change prices to percentage so it's comparable
chart.data.datasets.push({
label: data.meta.symbol,
data: prices.map((x) => ((x - prices[0]) / prices[0]) * 100),
});
chart.update();
document.getElementById(evt.detail.dataId).remove();
}

export { displayPrices, updateChart };
34 changes: 34 additions & 0 deletions app/static/js/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { displayPrices, updateChart } from "../../static/js/api.js";
import setupYahooPage from "../../static/js/page.yahoo.js";

const router = (evt) => {
let func;
switch (window.location.pathname) {
case "/analytics":
// dynamic modules example. not sure this is relevant if you bundle js
import("../../static/js/page.analytics.js").then((module) => {
module.default();
// custom trigger as this will lose race with load event
htmx.trigger("body", "pageReady", {});
});
break;
case "/yahoo":
func = setupYahooPage();
// custom event so that setup logic doesn't race hx-trigger: load
// technically not needed unless setup logic is absurdly long
document.body.dispatchEvent(new Event("pageReady"));
break;
}
};

// NOTE: addlistener should target named func to avoid dup listeners if called multiple times
// handle full page load
window.addEventListener("DOMContentLoaded", router);
// handle swap load
window.addEventListener("initPage", router);
// handle browser back/fwd. location.reload() on popstate works as well but is terrible solution
window.addEventListener("htmx:historyRestore", router);

// TODO: use single event and process based on response/url
window.addEventListener("displayPrices", displayPrices);
window.addEventListener("drawChart", updateChart);
56 changes: 1 addition & 55 deletions app/static/js/listeners.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,3 @@
// https://www.chartjs.org/docs/latest/developers/updates.html
function addData(chart, newData, labels) {
for (const data of newData) {
chart.data.datasets.push(data);
}
if (labels != null) {
chart.data.labels = labels;
}
chart.update();
}

function displayPrices(chart, table, data) {
const prices = data.indicators.quote[0].close;
// ideally, should check overlap of labels between datasets
table.updateOrAddData(data.timestamp.map((a, i) => ({ date: a, [data.meta.symbol]: prices[i] })));
if (!table.columnManager.columns.length) {
table.addColumn({ title: "Date", field: "date", frozen: true }, true);
}
table.addColumn({
title: data.meta.symbol,
field: data.meta.symbol,
hozAlign: "right",
headerSort: false,
formatter: "money",
formatterParams: { thousand: false },
});
chart.data.labels = data.timestamp;
// change prices to percentage so it's comparable
chart.data.datasets.push({
label: data.meta.symbol,
data: prices.map((x) => ((x - prices[0]) / prices[0]) * 100),
});
chart.update();
}

function setupResponseHandlers() {
document.body.addEventListener("displayPrices", function (evt) {
const chart = Chart.getChart(evt.detail.target);
const table = Tabulator.findTable("#priceTable")[0];
const data = JSON.parse(document.getElementById(evt.detail.dataId).textContent);
displayPrices(chart, table, data);
document.getElementById(evt.detail.dataId).remove();
});

// chartjs + htmx with payload in HX-Trigger
document.body.addEventListener("drawChart", function (evt) {
const chart = Chart.getChart(evt.detail.target);
// https://www.reddit.com/r/htmx/comments/10sdk43/comment/j72m2j7/
const data = JSON.parse(document.getElementById(evt.detail.dataId).textContent);
addData(chart, data.datasets, data.labels);
document.getElementById(evt.detail.dataId).remove();
});
}

const isKeyDown = (() => {
// https://stackoverflow.com/a/48750898
const state = {};
Expand All @@ -64,4 +10,4 @@ const isKeyDown = (() => {
return (key) => (Object.hasOwn(state, key) && state[key]) || false;
})();

export { isKeyDown, setupResponseHandlers };
export { isKeyDown };
53 changes: 53 additions & 0 deletions app/static/js/page.analytics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { arbLines } from "../../static/js/chart.plugins.arblines.js";
import { crosshairs } from "../../static/js/chart.plugins.crosshairs.js";
import { toggleView } from "../../static/js/chart.utils.js";

// https://stackoverflow.com/questions/67402685/vanilla-js-spa-how-to-load-specific-scripts-under-the-inserted-html-of-the-spa
// https://stackoverflow.com/questions/68554391/how-to-load-javascript-after-all-dom-element-rendered-in-vanilla-javascript
// https://stackoverflow.com/questions/54231533/how-to-create-a-vanilla-js-routing-for-spa
export default function setupAnalyticsPage() {
window.toggleView = toggleView;

new Chart(document.getElementById("chartId"), {
type: "bar",
options: {
responsive: true,
title: {
display: false,
text: "",
},
},
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
},
});

new Chart(document.getElementById("lineChartId"), {
type: "line",
plugins: [arbLines, crosshairs],
options: {
events: ["mousedown", "mouseup", "mousemove"],
responsive: true,
plugins: {
colors: {
forceOverride: true,
},
legend: {
position: "top",
},
title: {
display: true,
text: "Vehicles Sales",
},
tooltip: {
position: "average",
},
},
interaction: {
axis: "x",
intersect: false,
mode: "nearest",
},
},
});
}
57 changes: 57 additions & 0 deletions app/static/js/page.yahoo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { crosshairs } from "../../static/js/chart.plugins.crosshairs.js";

export default function setupYahooPage() {
new Chart(document.getElementById("priceChart"), {
type: "line",
plugins: [crosshairs],
options: {
events: ["mousedown", "mouseup", "mousemove"],
responsive: true,
layout: {
padding: {
left: 15,
},
},
scales: {
x: {
parsing: false,
type: "time", // to convert iso date to english
time: {
unit: "day",
},
},
},
plugins: {
colors: {
forceOverride: true,
},
legend: {
position: "top",
},
title: {
display: false,
text: "Security Prices",
},
tooltip: {
position: "nearest",
},
},
interaction: {
axis: "x",
intersect: false,
mode: "nearest",
},
},
});

new Tabulator("#priceTable", {
layout: "fitData",
index: "date",
pagination: true, //enable.
paginationSize: 10, // this option can take any positive integer value
placeholder: function () {
//set placeholder based on if there are currently any header filters
return this.initialised && this.getHeaderFilters().length ? "No Matching Data" : "No Data";
},
});
}
Loading

0 comments on commit d4181e3

Please sign in to comment.