Closed badlogic closed 1 year ago
Statehandling and component architecture will definitiv improve DX and future developments. vueJS, viteJS and tailwindCSS could work well.
why not go all out and redo it in Yew, might help to improve performance :P (also, I've heard Rust in frontend is like the newest of the hot new shit :D)
I'm honestly not sad about the current performance. The Tailwind PR has introduced noticable lag on mobile.
I looked into Vue and Svelt last night and they seem to be just a different form of spaghetti. Not sure we'd win anything.
On Wed, Jun 7, 2023, 02:51 simmac @.***> wrote:
why not go all out and redo it in Yew, might help to improve performance :P (also, I've heard Rust in frontend is like the newest of the hot new shit :D)
— Reply to this email directly, view it on GitHub https://github.com/badlogic/heissepreise/issues/82#issuecomment-1579672785, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAD5QBATYCDHEVSFDLW4CEDXJ7GCXANCNFSM6AAAAAAY5AEA7M . You are receiving this because you authored the thread.Message ID: @.***>
At the risk of bikeshedding which framework to use (well, this is an Internet discussion after all), you might as well throw React into the mix. Vite supports all of React, Svelte, Vue, …
Personally, the "plain old JavaScript" DX at this point is too complicated for me to dive deeper into the code. If you haven't used such JS frameworks before, it sure seems like adding a level of complication, but once you start reusing components like filters and tables and charts, it might make sense to attempt to switch. There is a learning curve involved — I had to learn React years ago, but now I wouldn't attempt to do a complex frontend project without it. A benefit you might get is that you can get open source contributions more easily because people can better understand how the project works. As a drawback, a rewrite is certainly not done in a day. And you are the maintainer so you should decide.
As for mobile performance, you have some performance optimization opportunities. This has nothing to with Tailwind itself, rather the large CDN request: #85
I love that the project is not a SPA and one could easily rewrite the current code to be cleaner this has nothing to do with a framework.
On that note I also like to use astro.js. Lots of small perks and you could use any framework. It would also work out of the box with the current project.
@badlogic if you are open for a compile process for the css I can add one, this will remove the performance issue.
I added a comment on issue #85 - if we use a framework, we should use one that @badlogic wants to learn, I am open to any framework, also more niche ones.
I'm happy I got everyone to shave the yak!
My goal would be to use the most minimal framework that allows componentization without the need for a build step. Something more structured but similar to the template literals currently used. Don't really need two way bindings. Bonus if it doesn't require a build step, altough we now have one for Tailwind anyways.
Haven't found a minimal framework yet.
If we use a bundler, I'd be in favor of esbuild. It's super simple and fast. Could then also go full TypeScript to alleviate some of the spaghetti.
For components, I don't know. I'm kind of thinking to roll my own tiny thing. I'll spend tonight investigating what's out there.
Do consider Vite. Gives you all the dev benefits of hot module reloading, bundling, etc. Works even with vanilla TypeScript (no component framework): https://stackblitz.com/edit/vitejs-vite-6uhgkf?file=index.html&terminal=dev
I would also vote for vite, we can keep vanillajs though
As I look more into this stuff I'm really not convinced this helps with anything other than introducing additional complexity. You're working with a guy who figured just sending all the data statically to the client is the best solution (and I honestly think it is, instead of setting up a shitton of infra to "scale out"). I understand I sound like an old man yelling at the kids. But hear me out.
The Tailwind addition looks great, but restyling is a nightmare. It also seems to have introduced a performance regression for large result sets especially on mobile.
Vue, React, Svelt etc. all require various preprocessors/compilers/build steps, which adds a lot of complexity for little gain.
The front end code is about 200 LOC per file. In addition to that there's around 700 LOC of utility code in utils.js (with another 600LOC for a German stemmer). Yes, it is spaghetti, but so would be a Vue/Svelt/React reimplementation, in addition to added build complexity. I honestly see no upside there.
Now, I too hate the code as it is. But I think a vanilla JS approach is still the best way to go, if we reorganize things.
I'm ok with introducing a build tool for the front end code. We already use PostCSS anyways. I'll look into Vite, but I might just end up with my old trusty esbuild.
With a bundler, we can more easily refactor things into compartmentalized components that aren't shat all over the place.
I'll spend more time tonight doing research. Some other old fart must have figured out a sane vanilla JS way for small projects like this that doesn't require you to install god damn dev tools as a browser extension just to be able to debug 200 LOC of JS/HTML/CSS.
On Wed, Jun 7, 2023, 18:46 Florian @.***> wrote:
I would also vote for vite, we can keep vanillajs though
— Reply to this email directly, view it on GitHub https://github.com/badlogic/heissepreise/issues/82#issuecomment-1581186813, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAD5QBFP4JRJNLV3EO7XKR3XKCV6DANCNFSM6AAAAAAY5AEA7M . You are receiving this because you were mentioned.Message ID: @.***>
Sure, I get that. The problem with vanilla JS is that you sometimes end up reinventing the wheel for problems that have been solved countless of times, maybe with a bit more convenience. For instance, I earlier wanted to change the code to allow for if/else templates but I figured it'd be just too much work for me. (Using things like Jinja2 for instance it would be trivial.)
Maybe you could consider JSDoc instead of full-blown TypeScript for type annotations.
I'll spend more time tonight doing research. Some other old fart must have figured out a sane vanilla JS way for small projects like this that doesn't require you to install god damn dev tools as a browser extension just to be able to debug 200 LOC of JS/HTML/CSS.
Whats you goal anyway? I think the current project could be refactored in vanilla JS without problems and no need for any frameworks. Most browsers already support web components, native modals etc. only thing which I always hate is that you cannot include easily templates without any tools as there is no native html into html web support.
As for your question this tools are maybe of interest to you handlebars, pug, gulp etc. but dunno if they are really needed.
I suppose I want
That's all very wishy washy, I guess I'll just build a thing and you folks can yell at me :D
So, part one is now complete. What's changed:
site/**/*.js
and *.html
still exist, as I'm slowly working my way through refactoring.${file}-new.html|js
, see carts-new.html|js
as an example where things are going. Also see the models/
folder.bundle.js
, the server calls into it. The GitHub pages generator in pages.js
also uses this.Long story short:
npm run dev
for dev mode to get live reloadnpm run start
for prod mode, which will bundle and minify things as well as possible.Next up, I'm going to rewrite the pages, split them up into re-usable components. Since esbuild allows me to require("file.js")
all the things, everything should improve maintenance wise. We'll see.
I've decided against a framework, as the site's components are pretty much trivial. The cognitive and maintenance load added by a framework is not worth any benefits in this project's case imo. Tailwind is enough suffering :D
Alright, I have created the first working component and page that uses it. See https://github.com/badlogic/heissepreise/commit/2e2df3db380175f04559532b487a4c6c82a78128
Basic idea:
models/model.js
a base class Model
all models extend from. You can add/remove listeners to/from the model. The model notifies all listeners of changes.models/carts.js
a model that stores carts. Adding and removing a cart will triger a save()
which in turn will notify all listeners. Modification of a cart's properties will not trigger a notification (yet).views/view.js
a base class for custom elements, aka web components, that takes a model, registers for change notifications on the model, and renders it.views/carts-lists.js
a view that shows carts in the carts model as a table. Registers itself as a custom element, so you can add it to your DOM via <carts-list>
.carts-new.html
a reimplementation of carts.html
with the new fancy stuff above.I think this is pretty clean, all things considered. I also think for our purpose the basic structure is more than good enough and should cover all our use cases nicely. Funnily enough, the code and markup of carts-new.html
is about the same size as the old implementation, while being much easier to maintain and reuse. Kinda validates the approach imo.
If there aren't any complaints, I'll convert the rest of the code to this new structure as well.
@badlogic Just a suggestion, feel free to disregard: Does it at some point make sense to extract the templates in to proper <template>
tags?
I don't know if you prefer co-locating HTML with functionality or not, but I've used this approach before and found it fairly pleasant to work with, because the render calls essentially just become a node-cloning operation and some element updates.
https://github.com/pretzelhands/ubiquitous-sniffle/blob/mster/client/index.html#L46 https://github.com/pretzelhands/ubiquitous-sniffle/blob/master/client/js/dom.js#L40
I'm doing that for child elements that are dynamic inside a component, e.g. entries in a list: https://github.com/badlogic/heissepreise/blob/main/site/views/carts-list.js#L74
I see no benefit using the template tag, except possibly shadow dom functionality, which I don't think we need. I like colocating the HTML with the (view) functionality.
Worked on this some more. Check out index-new.html
and changes-new.html
as entry points. Search for something that yields a gazillion results. Yay, infinite scroll. Now we can go crazy with tailwind and not suffer terrible style calculation times. Makes everything super responsive, including on mobile.
I've also created a framework of sorts. I've written up a small description here: https://twitter.com/badlogicgames/status/1667263838230663197
Quick re-cap given current state.
A Model holds data and notifies listeners if the data changes. We have two models: items and carts. Those aren't fully reactive, two-way bound things. Items notifies when its filteredItems
property changes. Carts notifies when the carts are saved to local storage. That's good enough as triggers to update the UI. See site/models.
A View is more interesting. It's a re-usable web component that (optionally) takes a model to display data from. A view sets up its basic, model independent UI in the constructor, usually by just setting innerHTML
. A view's state is actually stored in its UI elements. Yeah, don't @ me. By state, I'm not talking about actual data, but values of things like text fields, checked states, selected options, etc. Actual data goes into models. The view then displays and/or modifies the model it was assigned based on its state.
UI elements inside the view that hold state or with which you want to interact with somehow can be marked with an x-id
attribute. That should be an id that's unique within the view.
Use View.elements
to access all elements with an x-id
. It will return an map of x-id -> element
.
When a model is set, the view is automatically registered as a listener. When the model notifies the view of a change, the view's render()
method is called. The render()
method then (usually) takes the new model contents and updates the UI accordingly.
Mandatory shitty todo in 100 LOC:
<!DOCTYPE html>
<html>
<body style="display: flex; flex-direction: column; max-width: 600px; margin: 0 auto;">
<h2 style="text-align: center;">Shitty To-do</h2>
<todo-adder></todo-adder>
<todo-list></todo-list>
<script src='example.js'></script>
</body>
</html>
const { dom } = require("./misc");
const { Model } = require("./model/model");
const { View } = require("./views/view");
class TodoModel extends Model {
constructor() {
super();
try {
this._items = JSON.parse(localStorage.getItem("todos"));
} catch (e) {
this._items = [];
}
if (!this._items) this._items = [];
}
set items(items) {
this._items = items;
localStorage.setItem("todos", JSON.stringify(items));
this.notify();
}
get items() {
return this._items;
}
}
class TodoList extends View {
constructor() {
super();
this.innerHTML = /*html*/`
<div x-id="list" style="display: flex; flex-direction: column; margin-top: 1em;">
</div>
`;
this._itemTemplate = dom(
`div`,
/*html*/`
<span x-id="name" style="flex: 1;"></span>
<span x-id="date"></span>
<label style="margin-left: 10px;">
<input x-id="complete" type="checkbox"> Complete
</label>
<button x-id="remove" style="margin-left: 10px;">Remove</button>
`
);
this._itemTemplate.setAttribute("style", "display: flex; align-items: center; border: 1px solid #ccc; border-radius: 5px; padding: 0.5em; margin-top: 0.5em;");
}
render() {
const list = this.elements.list;
list.innerHTML = "";
this.model.items.forEach((todo, index) => {
let todoDom = this._itemTemplate.cloneNode(true);
let { name, date, complete, remove } = View.elements(todoDom);
name.innerText = todo.name;
if (todo.complete) name.style.textDecoration = "line-through";
date.innerText = todo.date;
complete.checked = todo.complete;
complete.addEventListener("change", () => {
todo.complete = complete.checked;
this.model.items.sort((a, b) => (a.complete == b.complete ? a.date.localeCompare(b.date) : a.complete ? 1 : -1));
this.model.items = this.model.items;
});
remove.addEventListener("click", () => {
this.model.items.splice(index, 1);
this.model.items = this.model.items;
});
list.append(todoDom);
});
}
}
customElements.define("todo-list", TodoList);
class TodoAdder extends View {
constructor() {
super();
this.innerHTML = /*html*/`
<div style="display: flex; flex-direction: row;">
<input x-id="name" type="text" style="flex: 1;" placeholder="What to do?">
<button x-id="add">Add</button>
</div>
`;
const elements = this.elements;
elements.add.addEventListener("click", () => {
let name = elements.name.value.trim();
if (name.length == 0) return;
let date = new Date().toDateString();
this.model.items.push({ name, date, complete: false });
this.model.items = this.model.items;
elements.name.value = "";
});
}
}
customElements.define("todo-adder", TodoAdder);
const todoModel = new TodoModel();
document.querySelector("todo-list").model = todoModel;
document.querySelector("todo-adder").model = todoModel;
There are a few more things:
x-change
, x-click
, x-input
, and x-input-debounce
generates their respective event. The outside listener doesn't need to know what exactly changed, just that the view's state changed. The outside listener can then access the state via the View.state
getter. For this to work, you have to call View.setupEventHandlers()
at the end of your view's constructor, or any time you add new elements with that markup, e.g. in View.render()
.x-state
will be present in the View.state
object.View.state
getter and setter to (de-)serialize the views state. Nice for things like our search filter settings.Check out the files in site/models
and site/views
along with index-new.js
and changes-new.js
. Should be straight forward, albeit not "reactive" in the sense that we observe all the things with proxies. Just simple listeners. Works well enough, 0 magic, and is very fast.
@flobauer @pretzelhands @simmac @HannesOberreiter @iantsch latest on main now has the all new front end. I think it's a slight improvement. In case of questions, ask here, but I think it should be straight forward.
Because fuck it, why not... Different kind of spaghetti.