badlogic / heissepreise

Jo eh.
MIT License
960 stars 116 forks source link

Rewrite front end in Vue #82

Closed badlogic closed 1 year ago

badlogic commented 1 year ago

Because fuck it, why not... Different kind of spaghetti.

flobauer commented 1 year ago

Statehandling and component architecture will definitiv improve DX and future developments. vueJS, viteJS and tailwindCSS could work well.

simmac commented 1 year ago

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)

badlogic commented 1 year ago

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: @.***>

slhck commented 1 year ago

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

HannesOberreiter commented 1 year ago

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.

flobauer commented 1 year ago

@badlogic if you are open for a compile process for the css I can add one, this will remove the performance issue.

flobauer commented 1 year ago

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.

badlogic commented 1 year ago

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.

badlogic commented 1 year ago

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.

slhck commented 1 year ago

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

flobauer commented 1 year ago

I would also vote for vite, we can keep vanillajs though

badlogic commented 1 year ago

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: @.***>

slhck commented 1 year ago

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.

HannesOberreiter commented 1 year ago

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.

badlogic commented 1 year ago

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

badlogic commented 1 year ago

So, part one is now complete. What's changed:

Long story short:

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

badlogic commented 1 year ago

Alright, I have created the first working component and page that uses it. See https://github.com/badlogic/heissepreise/commit/2e2df3db380175f04559532b487a4c6c82a78128

Basic idea:

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.

pretzelhands commented 1 year ago

@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

badlogic commented 1 year ago

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.

badlogic commented 1 year ago

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.

Models

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.

Views

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:

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.

badlogic commented 1 year ago

@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.