webtides / element-js

Simple and lightweight base classes for web components with a beautiful API
MIT License
28 stars 3 forks source link

[feat]: add media change listener to events map #38

Closed quarkus closed 1 year ago

quarkus commented 2 years ago

come up with a solution to listen to media changes in JS

i.e.

   mediaQueryList.addEventListener('change', event => {
            this.renderMobileTemplate = mediaQueryList.matches;
            this.requestUpdate().then(() => {
                this.evaluateState();
            });
        });
eddyloewen commented 2 years ago

I really like the idea of reactive controllers that lit added in v2. https://lit.dev/docs/composition/controllers/

As far as I understand it they are also supposed to work cross framework: https://github.com/lit/lit/issues/1682

I think it is a really neat solution to composing UI classes/elements. Would this maybe make sense for us to try to make this pattern work for element-js as well?

In case of the mediaQueryList change listener from the described issue - Something like this could work? https://lit.dev/playground/#project=W3sibmFtZSI6Im15LWVsZW1lbnQudHMiLCJjb250ZW50IjoiaW1wb3J0IHtMaXRFbGVtZW50LCBodG1sfSBmcm9tICdsaXQnO1xuaW1wb3J0IHtjdXN0b21FbGVtZW50fSBmcm9tICdsaXQvZGVjb3JhdG9ycy5qcyc7XG5pbXBvcnQge01vdXNlQ29udHJvbGxlcn0gZnJvbSAnLi9tb3VzZS1jb250cm9sbGVyLmpzJztcbmltcG9ydCB7TWF0Y2hNZWRpYUNvbnRyb2xsZXJ9IGZyb20gJy4vbWF0Y2gtbWVkaWEtY29udHJvbGxlci5qcyc7XG5pbXBvcnQge1NjcmVlblNpemVDb250cm9sbGVyfSBmcm9tICcuL3NjcmVlbi1zaXplLWNvbnRyb2xsZXIuanMnO1xuXG5AY3VzdG9tRWxlbWVudCgnbXktZWxlbWVudCcpXG5jbGFzcyBNeUVsZW1lbnQgZXh0ZW5kcyBMaXRFbGVtZW50IHtcbiAgcHJpdmF0ZSBtb3VzZSA9IG5ldyBNb3VzZUNvbnRyb2xsZXIodGhpcyk7XG4gIHByaXZhdGUgbWF0Y2hNZWRpYSA9IG5ldyBNYXRjaE1lZGlhQ29udHJvbGxlcih0aGlzLCAnKG1heC13aWR0aDogNjAwcHgpJyk7XG4gIHByaXZhdGUgc2NyZWVuU2l6ZSA9IG5ldyBTY3JlZW5TaXplQ29udHJvbGxlcih0aGlzKTtcblxuICByZW5kZXIoKSB7XG4gICAgcmV0dXJuIGh0bWxgXG4gICAgICA8aDM-VGhlIG1vdXNlIGlzIGF0OjwvaDM-XG4gICAgICA8cHJlPlxuICAgICAgICB4OiAke3RoaXMubW91c2UucG9zLnggYXMgbnVtYmVyfVxuICAgICAgICB5OiAke3RoaXMubW91c2UucG9zLnkgYXMgbnVtYmVyfVxuICAgICAgPC9wcmU-XG4gICAgICA8aDM-U2NyZWVuIHNpemU6PC9oMz5cbiAgICAgIDxwcmU-XG4gICAgICAgIHdpZHRoOiAke3RoaXMuc2NyZWVuU2l6ZS53aWR0aCBhcyBudW1iZXJ9XG4gICAgICAgIGhlaWdodDogJHt0aGlzLnNjcmVlblNpemUuaGVpZ2h0IGFzIG51bWJlcn1cbiAgICAgIDwvcHJlPlxuICAgICAgPGgzPk1lZGlhIFF1ZXJ5IExpc3Q6PC9oMz5cbiAgICAgIDxwcmU-XG4gICAgICAgIG1hdGNoZXM6ICR7dGhpcy5tYXRjaE1lZGlhLm1hdGNoZXN9XG4gICAgICA8L3ByZT5cbiAgICBgO1xuICB9XG59XG4ifSx7Im5hbWUiOiJtb3VzZS1jb250cm9sbGVyLnRzIiwiY29udGVudCI6ImltcG9ydCB7UmVhY3RpdmVDb250cm9sbGVySG9zdH0gZnJvbSAnbGl0JztcblxuZXhwb3J0IGNsYXNzIE1vdXNlQ29udHJvbGxlciB7XG4gIHByaXZhdGUgaG9zdDogUmVhY3RpdmVDb250cm9sbGVySG9zdDtcbiAgcG9zID0ge3g6IDAsIHk6IDB9O1xuXG4gIF9vbk1vdXNlTW92ZSA9ICh7Y2xpZW50WCwgY2xpZW50WX06IE1vdXNlRXZlbnQpID0-IHtcbiAgICB0aGlzLnBvcyA9IHt4OiBjbGllbnRYLCB5OiBjbGllbnRZfTtcbiAgICB0aGlzLmhvc3QucmVxdWVzdFVwZGF0ZSgpO1xuICB9O1xuXG4gIGNvbnN0cnVjdG9yKGhvc3Q6IFJlYWN0aXZlQ29udHJvbGxlckhvc3QpIHtcbiAgICB0aGlzLmhvc3QgPSBob3N0O1xuICAgIGhvc3QuYWRkQ29udHJvbGxlcih0aGlzKTtcbiAgfVxuXG4gIGhvc3RDb25uZWN0ZWQoKSB7XG4gICAgd2luZG93LmFkZEV2ZW50TGlzdGVuZXIoJ21vdXNlbW92ZScsIHRoaXMuX29uTW91c2VNb3ZlKTtcbiAgfVxuXG4gIGhvc3REaXNjb25uZWN0ZWQoKSB7XG4gICAgd2luZG93LnJlbW92ZUV2ZW50TGlzdGVuZXIoJ21vdXNlbW92ZScsIHRoaXMuX29uTW91c2VNb3ZlKTtcbiAgfVxufVxuIn0seyJuYW1lIjoiaW5kZXguaHRtbCIsImNvbnRlbnQiOiI8IWRvY3R5cGUgaHRtbD5cbjxodG1sPlxuICA8aGVhZD5cbiAgICA8c2NyaXB0IHR5cGU9XCJtb2R1bGVcIiBzcmM9XCIuL215LWVsZW1lbnQuanNcIj48L3NjcmlwdD5cbiAgPC9oZWFkPlxuICA8Ym9keT5cbiAgICA8bXktZWxlbWVudD48L215LWVsZW1lbnQ-XG4gIDwvYm9keT5cbjwvaHRtbD5cbiJ9LHsibmFtZSI6InBhY2thZ2UuanNvbiIsImNvbnRlbnQiOiJ7XG4gIFwiZGVwZW5kZW5jaWVzXCI6IHtcbiAgICBcImxpdFwiOiBcIl4yLjAuMFwiLFxuICAgIFwiQGxpdC9yZWFjdGl2ZS1lbGVtZW50XCI6IFwiXjEuMC4wXCIsXG4gICAgXCJsaXQtZWxlbWVudFwiOiBcIl4zLjAuMFwiLFxuICAgIFwibGl0LWh0bWxcIjogXCJeMi4wLjBcIlxuICB9XG59IiwiaGlkZGVuIjp0cnVlfSx7Im5hbWUiOiJtYXRjaC1tZWRpYS1jb250cm9sbGVyLnRzIiwiY29udGVudCI6ImltcG9ydCB7UmVhY3RpdmVDb250cm9sbGVySG9zdH0gZnJvbSAnbGl0JztcblxuZXhwb3J0IGNsYXNzIE1hdGNoTWVkaWFDb250cm9sbGVyIHtcbiAgcHJpdmF0ZSBob3N0OiBSZWFjdGl2ZUNvbnRyb2xsZXJIb3N0O1xuICBtYXRjaGVzID0gZmFsc2U7XG5cbiAgX29uTWF0Y2hNZWRpYUNoYW5nZSA9IChlOiBFdmVudCkgPT4ge1xuICAgIHRoaXMubWF0Y2hlcyA9IGUubWF0Y2hlcztcbiAgICB0aGlzLmhvc3QucmVxdWVzdFVwZGF0ZSgpO1xuICB9O1xuXG4gIGNvbnN0cnVjdG9yKGhvc3Q6IFJlYWN0aXZlQ29udHJvbGxlckhvc3QsIG1lZGlhUXVlcnk6IHN0cmluZykge1xuICAgIHRoaXMuaG9zdCA9IGhvc3Q7XG4gICAgY29uc3QgbXFsID0gd2luZG93Lm1hdGNoTWVkaWEobWVkaWFRdWVyeSk7XG4gICAgdGhpcy5tYXRjaGVzID0gbXFsLm1hdGNoZXM7XG4gICAgbXFsLmFkZEV2ZW50TGlzdGVuZXIoJ2NoYW5nZScsIHRoaXMuX29uTWF0Y2hNZWRpYUNoYW5nZSk7XG4gICAgaG9zdC5hZGRDb250cm9sbGVyKHRoaXMpO1xuICB9XG4gIFxuICBob3N0Q29ubmVjdGVkKCkge1xuICB9XG5cbiAgaG9zdERpc2Nvbm5lY3RlZCgpIHtcbiAgfVxufSJ9LHsibmFtZSI6InNjcmVlbi1zaXplLWNvbnRyb2xsZXIudHMiLCJjb250ZW50IjoiaW1wb3J0IHtSZWFjdGl2ZUNvbnRyb2xsZXJIb3N0fSBmcm9tICdsaXQnO1xuXG5leHBvcnQgY2xhc3MgU2NyZWVuU2l6ZUNvbnRyb2xsZXIge1xuICBwcml2YXRlIGhvc3Q6IFJlYWN0aXZlQ29udHJvbGxlckhvc3Q7XG4gIHdpZHRoID0gMDtcbiAgaGVpZ2h0ID0gMDtcblxuICBfb25SZXNpemUgPSAoKSA9PiB7XG4gICAgdGhpcy53aWR0aCA9IHdpbmRvdy5pbm5lcldpZHRoO1xuICAgIHRoaXMuaGVpZ2h0ID0gd2luZG93LmlubmVySGVpZ2h0O1xuICAgIHRoaXMuaG9zdC5yZXF1ZXN0VXBkYXRlKCk7XG4gIH07XG5cbiAgY29uc3RydWN0b3IoaG9zdDogUmVhY3RpdmVDb250cm9sbGVySG9zdCkge1xuICAgIHRoaXMuaG9zdCA9IGhvc3Q7XG4gICAgdGhpcy5fb25SZXNpemUoKTtcbiAgICB3aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcigncmVzaXplJywgdGhpcy5fb25SZXNpemUpO1xuICAgIGhvc3QuYWRkQ29udHJvbGxlcih0aGlzKTtcbiAgfVxuICBcbiAgaG9zdENvbm5lY3RlZCgpIHtcbiAgfVxuXG4gIGhvc3REaXNjb25uZWN0ZWQoKSB7XG4gIH1cbn0ifV0

quarkus commented 2 years ago

That looks cool, but i think it would be quite a change if we'd introduce these... I mean we could replace the entire event map with these kind of controllers.

For now i had sth like this in mind:

events() {
    return {
        window : {
            'media(max-width: 600px)': () => {
                //did change
            }
        }
    }
}

Thats way simpler but and would play nicely with what we have right now. But it has the downside that you can't compare an initial "matches" or access the mq instance ...

eddyloewen commented 2 years ago

No I was just thinking if this controllers thing could be something that we would like for element-js in general as well 👍

But yeah - for this specific feature we can think of something for now.

How about something like this?

isMobile = window.matchMedia('(max-width: 600px)');

connected() {
    console.log('connected', this.isMobile.matches);
}

events() {
    return {
      'media:isMobile': {
        'change': (e) => {
          console.log('change', e);
        }        
      }
    }
}

Where element-js would know if the "selector" starts with "media:" it will look for the property on the instance and add the listener.

eddyloewen commented 2 years ago

We could also provide a matchMediaQuery helper:

import { BaseElement, matchMediaQuery } from '@webtides/element-js';

class ExampleElement extends BaseElement {
    isMobile = matchMediaQuery('(max-width: 600px)', (e)   => {
      console.log('change', e);
    });

    connected() {
      console.log('connected', this.isMobile.matches);
    }
}
eddyloewen commented 2 years ago

Or we could do something like the controller pattern and call the lifecycle implicitly:

import { TemplateElement, matchMediaQuery } from '@webtides/element-js';

class ExampleElement extends TemplateElement {
    isMobile = matchMediaQuery(this, '(max-width: 600px)');

    connected() {
      console.log('connected', this.isMobile.matches);
    }

    template() {
      return html`<div>${this.isMobile.matches ? 'is mobile' : 'is not mobile'}</div>`;
    }
}

In this case the helper function would add the listener behind the scenes and call the "requestUpdate" method on the element.

lukas-schardt commented 2 years ago

I like the idea, but i don't like putting it inside the event map, or at least not on the window. That could be confusing as they are not really these classic events.

window already is a special key, maybe we can put media as another special key?

events() {
    return {
        'media': {
             '(min-width: 300px)': (matcher) => { },
        }
    }
}

Or another map:

media() {
    return {
        '(min-width: 300px)': (matcher) => { },
    };
}

@eddyloewen For now i think i would like to stay in the element-js pattern and not introduce a whole new concept of doing things. But for the future this controller pattern seem to be worth a shot. 👍

lukas-schardt commented 2 years ago

How would we handle the initial state?

quarkus commented 1 year ago

MediaStore Example is now in docs: https://github.com/webtides/element-js/commit/5718c085813b6a51d12a13e93edba8e42e87a09c