WebReflection / uhtml

A micro HTML/SVG render
MIT License
922 stars 37 forks source link

How nested signals resolution should work? #140

Open m-michalis opened 3 weeks ago

m-michalis commented 3 weeks ago

The bellow is a simple example of how i would expect (?) nested signals to work.

When clicking "to cart" it should change to "loading".

<html>
<body>
<div id="products"></div>
<script type="module">
    import {render, signal, html, effect, computed} from 'https://esm.run/uhtml/signal';

    class Cart {
        add(product) {
            console.log('adding to cart: ', product.name);
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    console.log('added to cart: ', product.name);
                    resolve();
                }, 2000);
            })
        }
    }

    const cart = new Cart();

    class Item {
        constructor(sData) {
            this.data = sData;
        }

        renderItem() {
            return html`
                <li>${this.data.value.name}
                    <button @click="${e => {
                        this.data.value.loading_cart = true
                        cart.add(this.data.value).then(() => {
                            this.data.value.loading_cart = false
                        });
                    }}">${this.data.value.loading ? 'loading' : 'to cart'}
                    </button>
                </li>`;
        }
    }

    class Catalog {
        #items = signal([]);
        #computed;
        #effect;

        constructor() {

            this.#computed = computed(() => this.#items.value.map(item => (new Item(signal(item))).renderItem()));
            this.#effect = effect(() => {
                render(document.getElementById('products'), () => html`
                    <ul>${this.#computed.value}</ul>`)
            });
        }

        addItem(value) {
            this.#items.value = [...this.#items.value, value];
        }

        setItems(items) {
            this.#items.value = items;
        }
    }

    const catalog = new Catalog();

    setTimeout(() => {
        catalog.setItems([
            {name: 'Shoes'},
            {name: 'T-Shirt'}
        ])
        setTimeout(() => {
            catalog.addItem({name: 'Hat'})
            catalog.addItem({name: 'Boots'})
        }, 1500)
    }, 500)

</script>
</body>
</html>

What would be a working example for the above?

WebReflection commented 3 weeks ago

I think you need to re-assign value to trigger a signal, otherwise you are just updating its internal state without communicating any change happened at all?

this.data.value.loading_cart = false
this.data.value = this.data.value

Signals need to notify their value, what's inside their value is never observed ... would that work?

WebReflection commented 3 weeks ago

P.S. if that doesn't work due meomized value, try:

this.data.value = {...this.data.value};

assuming that data.value is just, indeed, data and nothing else ... pass a new thingy otherwise with new values.

m-michalis commented 3 weeks ago

it seems that it switches button text to "loading", but it doesnt trigger back.

Updated the @click to simulate waiting time.

(also there was a typo on the example. fixed bellow)

class Item {
        #data = signal({});

        constructor(sData) {
            this.#data.value = sData;
        }

        cartLoading(state = false){
            if(this.#data.value.loading_cart !== state) {
                this.#data.value.loading_cart = state
                this.#data.value = {...this.#data.value}
            }
            return this;
        }

        renderItem() {
            return html`
                <li>${this.#data.value.name}
                    <button @click="${e => {
                        this.cartLoading(true)
                        setTimeout(()=>{
                            this.cartLoading(false) // <-- this does not trigger the refresh again
                        },2000)
                    }}">${this.#data.value.loading_cart ? 'loading' : 'to cart'}
                    </button>
                </li>`;
        }
    }
WebReflection commented 3 weeks ago

this does not trigger the refresh again

what if you remove if(this.#data.value.loading_cart !== state) ?

there are various versions of signals in uhtml so if you are anyhow stuck try other variants with preact signals, as example, and let me know if that solves ... if it does, it's my own signal implementation (a different repo) that is doing something weird, if it doesn't, it's your signals based logic that is doing something weird ... I am exploring a rewrite of this library that is currently focused on other topics so my time is limited for this issue but I'd love to understand where, how, this doesn't work, as long as the underlying logic is sound, thanks for your comprehension