Felix Riesterer: Meinungen zu meinem Widget

problematische Seite

Liebe Mitlesende,

heute habe ich ausprobiert, ob ich mir ein sogenanntes Widget selbst schreiben kann: Einen Button, der selbst nachfragt, ob man wirklich die Aktion durchführen möchte. Das Ganze konfigurierbar, damit man eigene Aktionen an diesen Button anbringen kann.

In diesem Script verwende ich zum ersten Mal private Objekteigenschaften in JavaScript. Das Teil ist also zum Selbststudium gewesen, auch wenn der Anlass ein konkreter Bedarf bei der Entwicklung meiner Webanwendung war.

Habt ihr Kritik für mich?

Liebe Grüße

Felix Riesterer

  1. problematische Seite

    Servus!

    Liebe Mitlesende,

    heute habe ich ausprobiert, ob ich mir ein sogenanntes Widget selbst schreiben kann: Einen Button, der selbst nachfragt, ob man wirklich die Aktion durchführen möchte. Das Ganze konfigurierbar, damit man eigene Aktionen an diesen Button anbringen kann.

    Ich hätte es evtl in Web Components als <confirm-dialog> angelegt. Was hältst du davon?

    Siehe hier: https://www.webcomponents.org/element/jifalops/confirm-dialog

    Herzliche Grüße

    Matthias Scharwies

    --
    Die Signatur findet sich auf der Rückseite des Beitrags.
    1. problematische Seite

      Lieber Matthias,

      Ich hätte es evtl in Web Components als <confirm-dialog> angelegt. Was hältst du davon?

      Siehe hier: https://www.webcomponents.org/element/jifalops/confirm-dialog

      auf der Seite lädt bei mir die Live-Demo nicht. Auf der Github-Seite konnte ich es dann ausprobieren. Aber einen Confirm-Dialog wollte ich ja gezielt nicht, weil der den Rest der Seite unerreichbar macht. Das Widget sollte nur an dieser Stelle eine Nachfrage stellen, ohne den Benutzer zu zwingen, weiter damit zu interagieren.

      Liebe Grüße

      Felix Riesterer

      1. problematische Seite

        Servus!

        Lieber Matthias,

        Ich hätte es evtl in Web Components als <confirm-dialog> angelegt. Was hältst du davon?

        Siehe hier: https://www.webcomponents.org/element/jifalops/confirm-dialog

        auf der Seite lädt bei mir die Live-Demo nicht.

        Bei mir auch nicht. Ich hatte nur nach deinem Problem gesucht. Das war die einzige Realisierung mit Web Components. Es gab mehrere Links, die auf entsprechende Lösungen in Frameworks verwiesen.

        Auf der Github-Seite konnte ich es dann ausprobieren. Aber einen Confirm-Dialog wollte ich ja gezielt nicht, weil der den Rest der Seite unerreichbar macht.

        Wenn Du einen Button hast und dann nachfragst, ob jemand sicher ist, ist das imho ein confirm.

        Ja, ein dialog-Element (und auch das JS-eigene confirm() sind modal, d.h. dass die Webseite kurzzeitig inaktiv ist, bis der dialog beendet ist.

        Das Widget sollte nur an dieser Stelle eine Nachfrage stellen, ohne den Benutzer zu zwingen, weiter damit zu interagieren.

        Du hast ja das gleiche Problem schon hier lösen wollen: JavaScript/Tutorials/Eigene_modale_Dialogfenster

        Hier hatte @Gunnar Bittersmann erkannt, dass du ja eigentlich nicht-modale Dialoge anbieten willst.

        Deshalb biete ich jetzt <nonmodal-confirm> als Name an.

        Matthias' Idee einer Web-Component klingt auch verlockend - aber kann man die so einfach selbst mit Styles erweitern? Da bin ich noch nicht so im Thema drin.

        Ja, das ist ja die Idee von Web Components, dass man ein Element hinzufügt, das wie video (mit dem eingebauten control-Elementen) oder input type="range" mit dem Schieberegler Kindelemente hat, die fest im Shadow Dom gekapselt sind.

        Natürlich kann man die mit Styles erweitern! Evtl würde ich anstelle der im Stylesheet festgelegten Farben für Warnungen und ok Systemfarben verwenden?

        Herzliche Grüße

        Matthias Scharwies

        --
        Die Signatur findet sich auf der Rückseite des Beitrags.
        1. problematische Seite

          Guten Morgen,

          Auf Discord hatten wir ja schon über @Hörnchen s <toggle-switch> diskutiert. Den gibt es auch als Vorschlag der open-ui: Switch (Explainer) [1]

          Hier gibt's ne Stelle:

          A switch differs from a checkbox because:

          • It has no indeterminate state
          • It requires an immediate action, no confirmation is needed for the state to take effect

          Also nach button + confirmation + "Web component" gesucht:

          Der Rest sind Frameworks.

          Wegen Modal vs. nonmodal hier noch ein Grundlagenartikel:

          Modal & Nonmodal Dialogs: When (& When Not) to Use Them) (nngroup.com)

          BTW: dialog.show() öffnet einen nichtmodalen Dialog, also könnte man auch dieses Element als Teil eines WebComponents verwenden.

          Also <conform-dialog inline> ? :-)

          Herzliche Grüße

          Matthias Scharwies

          --
          Die Signatur findet sich auf der Rückseite des Beitrags.

          1. Habe mal nicht nur die proposals, sondern auch alle research-items (Links unten durchsucht. Da gibt's alert, tag und toast - ich muss mehr mit Frameworks arbeiten 😉 ↩︎

  2. problematische Seite

    Hallo Felix Riesterer,

    Habt ihr Kritik für mich?

    Ja. Als erster Schritt nicht schlecht, aber praxistauglich ist es noch nicht.

    Die Stelle, wo der Button hin soll, MUSS im HTML durch Markup markiert werden. Idealerweise durch einen delete-Button, der durch eine Klasse für das Widget markiert wird und den das Widget dann zum Autoconfirm-Button umbaut. Alternativ überlässt man die Identifikation der Buttons dem Anwender und verlangt, dass er einfach einmal pro gewünschten Button das Widget darauflegt.

    Sowas tut man entweder per Konstruktor (new Widget(elem)) oder per Klassenfunktion (Widget.on(elem)). Das Widget kann auch einfach eine Funktion sein, die den Rest erledigt (und intern gerne OO benutzt)

    Texte sollten im Markup pro Button individuell vergeben werden können. Es spricht natürlich auch nichts dagegen, wenn das Widget eine zentrale Stelle für anpassbare Defaulttexte hat. Das Widget selbst sollte aber als ECMAScript-Modul importiert werden und nur die Konstruktorfunktion bereitstellen. Hat den Vorteil, dass auch der Verwender ein ECMAScript-Modul schreiben muss und damit die DOMContentLoaded-Kapriole erspart bleibt.

    <p>Sie haben drölf Dokumente ausgewählt
    <button id="deleteStuff" class="autoconfirm" data-confirm="Ja, Löschen" type="button">Löschen</button>
    <!-- optional data-query und data-cancel zum Überschreiben der Defaults -->
    </p>
    
    import AutoConfirmButton from "./autoconfirm_widget.js";
    
    AutoConfirmButtom.setDefaults({
       className: "autoconfirm",
       queryText: "Sind Sie sicher?",
       cancelText: "Abbrechen"
    });
    
    document.querySelectorAll("button.autoconfirm")
            .forEach(AutoConfirmButton);
    // oder vielleicht
    document.querySelectorAll("button.autoconfirm")
            .forEach(AutoConfirmButton.attach);
    // oder
    AutoConfirmButton.attach(".autoconfirm");
    

    Beim attach wird das DOM umgebaut, so dass der Button in ein span eingehüllt wird und der Cancel-Button darin versteckt wird.

    Es könnte auch eine Variante B geben, wo man das HTML komplett vorgibt (damit man es stylen kann) und die JS Lib nur die Steuerung übernimmt.

    Matthias' Idee einer Web-Component klingt auch verlockend - aber kann man die so einfach selbst mit Styles erweitern? Da bin ich noch nicht so im Thema drin.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. problematische Seite

      Lieber Rolf,

      vielen Dank für Deine Anmerkungen. Die individuell einstellbaren Callback-Funktionen können nur dann eingesetzt werden, wenn das Widget nicht rein aus data-Attributen gefütterte Einstellungen erhält. Oder wie dachtest Du, dass das Abbrechen oder das Ausführen dann via JavaScript weiter verarbeitet wird?

      In meinem GUI für das aktuelle Projekt baue ich alle Buttons ohnehin mit JavaScript, weil ein ursprünglich in der Seite enthaltenes Formular komplett durch ein Drag&Drop-lastiges GUI ersetzt wird.

      Mit ECMAScript-Modulen habe ich mich noch nicht beschäftigt.

      Liebe Grüße

      Felix Riesterer

      1. problematische Seite

        Hallo Felix,

        um tatsächlich qualifiziert antworten zu können, muss ich wohl eine eigene Version bauen 😉

        Wenn eine Seite ohne Javascript ohnehin funktionslos ist, kann man das HTML sicherlich auch per Script generieren. Ich hoffe nur, dass du auch eine Alternative für LuLuSuS, die auf Assistenztechnik angewiesen sind. D&D ist nichts für Screenreader, würde ich annehmen. Weder als RPG[1] noch als Bedientechnik

        Rolf

        --
        sumpsi - posui - obstruxi

        1. Nein, nicht die Rocket Propelled Grenade ↩︎

      2. problematische Seite

        Hallo Felix,

        so, ich habe jetzt eine Testseite gemacht, die so aussieht (kann sie gerade nicht auf meine Homepage hochladen):

        <!doctype html>
        <html>
        <head>
        <script type="module">
        import { ConfirmationButton } from "./modules/ConfirmationButton.js";
        
        for (const btn of document.querySelectorAll("button.autoconfirm")) {
           new ConfirmationButton(btn);
        }
        
        const deleteButton = document.querySelector("#deleteMarked")
        deleteButton.addEventListener("click", () => alert("Weg damit!"););
        deleteButton.addEventListener("cancel", () => alert("Na, dann nicht!"));
        </script>
        </head>
        <body>
        <h1>Testseite für den Confirmation-Button</h1>
        <p>Sie haben drölf Dokumente markiert
        <button id="deleteMarked" class="autoconfirm" data-prompt=" - wirklich Löschen?" type="button">Löschen</button></p>
        </body>
        </html>
        

        Das ist der Wireframe für das Widget und keine Produktionsseite, Gunnar! Erzähl also bitte nichts von lang oder meta viewport oder sonstigem Kram. Erzählen kannst Du mir, ob und wie der Autoconfirm-Button noch ariasiert werden muss, damit die Zugänglichkeit gewahrt bleibt.

        Wenn diese HTML Datei im Ordner /foo ist, dann erwartet sie das zu importierende Script im Ordner /foo/modules. Das Loader-Script muss type="module" sein, damit es importieren kann. Damit geht ein defer-Loading einher, also erst, wenn das DOM bereit ist.

        Ich hätte auch so vorgehen können, dass ich die Autoconfirm-Buttons automatisch vom Widget suchen lasse. Aber ich finde, dass hier das DOT-Prinzip wichtig ist: Do One Thing. Der Konstruktor macht aus einem button einen Autoconfirm-Button. Weiter nichts. Das Auffinden der Buttons ist Another Thing. Das Registrieren von Eventhandlern auf den Buttons ebenfalls, damit muss sich das Widget nicht befassen.

        Was das Widget tut, ist das Triggern eines cancel-Events, wenn der Abbrechen-Button gedrückt wird. Das gehört zum Verhalten des Autoconfirm-Buttons. Aber das Registrieren von Handlern nicht.

        Das Script modules/ConfirmationButton.js sieht so aus:

        export class ConfirmationButton {
          static #defaults = {
            confirmationClass: 'confirming',
            inline: true,
            confirmPrompt: "Wirklich löschen?",
            cancelText: "Abbrechen",
          };
          // todo: static API zum Setzen der Defailts
        
          #confirmedButton;
          #cancelPrompt;
          #cancelButton;
          #settings;
          constructor(button, settings) {
            if (!button instanceof HTMLButtonElement)
              throw new TypeError("This class can only be used for button elements");
            
            this.#confirmedButton = button;
        
            // defaults-Objekt als Prototyp für das settings-Objekt verwenden
            this.#settings = Object.create(ConfirmationButton.#defaults);
            // dann die übergebenen Settings hineinkopieren.
            Object.assign(this.#settings, settings);
        
            if (button.dataset.prompt)
              this.#settings.confirmPrompt = button.dataset.prompt;
            if (button.dataset.cancel)
              this.#settings.cancelText = button.dataset.cancel;
        
            const promptText = this.#settings.confirmPrompt;
            const cancelText = this.#settings.cancelText;
        
            this.#cancelButton = document.createElement("button");
            this.#cancelButton.type = "button";
            this.#cancelButton.textContent = cancelText;
            this.#cancelButton.style.marginLeft = this.#cancelButton.style.marginRight = "0.5em";
            this.#cancelPrompt = document.createElement("span");
            this.#cancelPrompt.textContent = this.#settings.confirmPrompt;
            this.#showCancelGroup(false);
                
            this.#confirmedButton.insertAdjacentElement("beforebegin", this.#cancelPrompt);
            this.#confirmedButton.insertAdjacentElement("beforebegin", this.#cancelButton);
             
            this.#confirmedButton.addEventListener("click",
                   this.#handleConfirmRequest.bind(this));
            this.#cancelButton.addEventListener("click",
                   this.#handleCancelRequest.bind(this));
          }
           
          #handleConfirmRequest(event) {
            if (!this.#confirmedButton.classList.toggle(this.#settings.confirmationClass)) {
              this.#showCancelGroup(false);
              return;
            }
            this.#showCancelGroup(true);
            event.stopImmediatePropagation();
            event.preventDefault();
          }
            
          #handleCancelRequest(event) {
            this.#confirmedButton.classList.remove(this.#settings.confirmationClass);
            this.#showCancelGroup(false);
            this.#confirmedButton.dispatchEvent(
              new Event("cancel", { bubbles: true, cancelable: true })
            );
          }
          #showCancelGroup(show) {
            this.#cancelPrompt.hidden = !show;
            this.#cancelButton.hidden = !show;
          }
        }
        

        Ob das jetzt besser ist als dein Ansatz, sei dahingestellt. Aber so würde ich es halt machen.

        Rolf

        --
        sumpsi - posui - obstruxi
        1. problematische Seite

          Lieber Rolf,

          Ob das jetzt besser ist als dein Ansatz, sei dahingestellt. Aber so würde ich es halt machen.

          für meine Zwecke scheint das weniger nützlich zu sein. Wie kann ich einem solchen ConfirmButton für den jawoll-Fall eine individuelle Funktion zur Ausführung geben? Die Klasse am button-Element führt ja nur dazu, dass der Button generell durch ein Widget ersetzt wird, nicht aber, was es im Klick-Falle tun soll.

          Liebe Grüße

          Felix Riesterer

          1. problematische Seite

            Hallo Felix Riesterer,

            Wie kann ich einem solchen ConfirmButton für den jawoll-Fall eine individuelle Funktion zur Ausführung geben?

            So, wie ich es eingebaut habe?

            Mein Ansatz wäre: Bau nichts nach, was schon da ist. Der Löschen-Button ist bereits ein EventTarget und kann daher 1-N Eventlistener bekommen.

            In #handleConfirmRequest siehst Du für den Fall, dass das Widget nicht im Bestätigungsmodus ist, die Aufrufe von stopImmediatePropagation und preventDefault, d.h. das click-Event auf dem Löschbutton wird weggefangen. Klickt man nochmal, wird das Widget wieder versteckt und der Klick kommt durch.

            Das klappt natürlich nur, wenn zuerst das Widget auf den Button gelegt wird und dann erst die Anwendungs-Events registriert werden.

            Rolf

            --
            sumpsi - posui - obstruxi
          2. problematische Seite

            Hallo Felix,

            wenn Du die Logik mehr beisammenhalten willst, kann man es auch so machen:

            const deleteButton = document.querySelector("#deleteMarked")
            new ConfirmationButton(deleteButton);
            deleteButton.addEventListener("click", () => alert("Weg damit!"););
            deleteButton.addEventListener("cancel", () => alert("Na, dann nicht!"));
            

            Statt querySelector könntest Du den Button an dieser Stelle auch dynamisch erzeugen...

            Vielleicht stört es Dich, dass ich das mit new erzeugte Objekt nicht speichere. Aber das ist kein Problem. Da Methoden dieses Objekts als Eventlistener ins DOM gehängt werden, existieren Referenzen auf das Objekt so lange, wie der Button im DOM ist.

            Falls Du den Button auch wieder dynamisch entfernen willst, musst Du allerdings etwas vorsichtiger sein, dann musst Du auch den Prompt-Span und den Cancel-Button wieder entfernen. Dafür habe ich jetzt noch keinen Mechanismus eingebaut. Mein Ansatz wäre dann, am HTMLButtonElement des Delete-Buttons ein eigenes Property zu verstecken (mit einem Symbol als Schlüssel) und dort das ConfirmationButton-Objekt zu hinterlegen. Dieses könnte dann mit einer detach-Methode den alten Zustand wiederherstellen.

            Rolf

            --
            sumpsi - posui - obstruxi
  3. problematische Seite

    Hallo Felix,

    warum verwendest du (Rolf habe ich schon beim Stammtisch gefragt) nicht die Technik für Custom Elements?

    Gruß
    Jürgen