Share Data between Riot Components with Mitt (Event Emitter)

Steeve - Apr 14 - - Dev Community

This article covers using Events with Mitt between multiple Riot components for sharing data.

Before starting, make sure you have a base application running, or read my previous article Setup Riot + BeerCSS + Vite.

These articles form a series focusing on RiotJS paired with BeerCSS, designed to guide you through creating components and mastering best practices for building production-ready applications. I assume you have a foundational understanding of Riot; however, feel free to refer to the documentation if needed: https://riot.js.org/documentation/

It exist three methods for sharing data between components:

  • Use Riot properties (props) to pass values to a child component. The child component must emit Events to the parent component if an action happens, such as a click or input change. In this case, the communication scope is limited: parent component to child and child to parent.
  • Use a state manager, such as Pinia for Vuejs or Redux for React, to share a state across components/pages. The communication scope is global: all components can view and edit states.

The last method uses an Event Emitter like Mitt, a messaging pattern called Pub/Sub: one or many publishers emit the data, and the listener (subscriber) handles it without knowing which publishers. This article reproduces the pattern between multiple RiotJS components! The goal is to:

  • Step 1: Create a base Riot application showing a Confirmation Modal component when clicking a button. The button is the publisher, and the Modal is the subscriber.
  • Step 2: When the modal is visible and either validated or cancelled, it emits an event to the original publisher. In this case, the Modal is the publisher, and the Button is the subscriber.

Animation of a Riot confirmation modal opening due to a Mitt event

Emitter Base

First install Mitt into your Riot/Vite project:

$ npm install mitt
Enter fullscreen mode Exit fullscreen mode

Create a file named events.js then load Mitt and instantiate it:

/** EVENT BUS **/
import mitt from 'mitt'

const _bus = mitt();

export default _bus;
Enter fullscreen mode Exit fullscreen mode

Dialog Component (Subscriber)

Create a dialog component named c-dialog.riot, the base of the component is taken from my previous article Create a Dialog component with RiotJS:

<c-dialog>
    <dialog class="{state.active ? 'active ' : null}">
        <h5>{ state.title }</h5>
        <div>
            { state.message }
        </div>
        <nav class="right-align no-space">
            <button onclick={ toggleModal } class="transparent link">{ state.cancel }</button>
            <button onclick={ validateAction } class="transparent link">{ state.validate }</button>
        </nav>
    </dialog>
       <script>
       import events from './events.js'

        export default {
            state: {
                active: false,
                title: 'Confirmation',
                message: 'Are you sure?',
                callbackID : '',
                args : true,
                validate : 'Validate',
                cancel: "Cancel"
            },
            onMounted() {
                events.on('open-modal-validation', this.openModalValidation);
            },
            onUnmounted() {
                events.off('open-modal-validation', this.openModalValidation);
            },
            validateAction () {
                if (this.state.callbackID) {
                    events.emit(this.state.callbackID, this?.state?.args);
                }
                this.toggleModal();
            },
            openModalValidation ({ message, callbackID, args }) {
                this.update({
                    message,
                    callbackID,
                    args: args ?? true
                })
                this.toggleModal();
            },
            toggleModal() {
                this.update({ active: !this.state.active })
                if (this.state.active === true) {
                    console.log("Modal opened.")
                } else {
                    console.log("Modal closed.")
                }
            }
        }
    </script>
</c-dialog>
Enter fullscreen mode Exit fullscreen mode

Source Code: https://github.com/steevepay/riot-beercss/blob/main/examples/mitt/c-dialog.riot

Code breakdown:

  • The modal is visible only if the state.active is true.
  • The dialog can be customised with custom data, it is stored in the State Riot object
  • The event bus is loaded with import events from './events.js'.
  • When the component is mounted, it subscribes to the event open-modal-validation thanks to events.on('open-modal-validation', this.openModalValidation);. When the event named "open-modal-validation" occurs, it will execute the function openModalValidation.
  • The openModalValidation function toggles the visibility of the modal by setting state.active to true. At the same time, it gather additional data such as a custom state.message, and custom state.callbackID.
  • If the validation button is clicked, it emits an Event to the state. callback with custom arguments state.args; it can be an Object or String.
  • If the cancel button is clicked, the modal is deactivated by setting state.active to false.

Index Component (Publisher)

Finally, load the Dialog, and a Button into an index.riot file:

<index-riot>
    <div style="width:600px;padding:20px;">
        <c-dialog />
        <c-button onclick={ confirmDeletion }> Delete File </c-button>
    </div>
    <script>
        import cButton from "../../components/c-button.riot";
        import cSnackbar from "../../components/c-snackbar.riot";
        import cDialog from "./c-dialog.riot";

        import events from "./events.js"

        export default {
            components: {
                cButton,
                cDialog
            },
            onMounted() {
                events.on('delete-file', this.deleteFile);
            },
            onUnmounted() {
                events.off('delete-file', this.deleteFile);
            },
            state: {
                active: false
            },
            confirmDeletion () {
                events.emit('open-modal-validation', { 
                    message: "Do you confirm you want to delete the file \"2024-04-invoice.pdf\"?",
                    callbackID: 'delete-file'
                });
            },
            deleteFile () {
                console.log("FILE DELETED! 🗑️");
            }
        }
    </script>
</index-riot>
Enter fullscreen mode Exit fullscreen mode

Source Code: https://github.com/steevepay/riot-beercss/blob/main/examples/mitt/index.riot

Code details:

  1. Components are imported with import cDialog from "./components/c-dialog.riot"; then loaded in the components:{} Riot object.
  2. The dialog component is instantiated with <c-dialog/> on the HTML.
  3. The event emitter, Mitt, is loaded with import events from "./events.js"
  4. When clicking the button, the function confirmDeletion is executed and emits the event "open-modal-validation". The Dialog is now visible! Simultaneously, a custom message and the subscriber ID "delete-file" are passed as arguments for the event.
  5. When the dialog button is clicked, an event is emitted, using the state.callbackID as the destination subscriber.

This method can be extended by creating more buttons (publishers) emitting to only one Dialog (subscriber), for instance:

Three buttons are opening the same Dialog box to confirm deletions actions, either: a file, a bucket or an account

Here is the code of the index.riot to replicate the GIF demo:

<index-riot>
    <div style="width:600px;padding:50px;">
        <c-dialog />
        <c-button onclick={ confirmDeletion }> Delete File </c-button>
        <c-button onclick={ confirmDeletionBucket }> Delete Bucket </c-button>
        <c-button error={ true } onclick={ confirmDeletionAccount }> Delete Account </c-button>
    </div>
    <script>
        import cButton from "../../components/c-button.riot";
        import cSnackbar from "../../components/c-snackbar.riot";
        import cDialog from "./c-dialog.riot";

        import events from "./events.js"

        export default {
            components: {
                cButton,
                cDialog
            },
            onMounted() {
                events.on('delete-file', this.deleteFile);
                events.on('delete-bucket', this.deleteBucket);
                events.on('delete-account', this.deleteAccount);
            },
            onUnmounted() {
                events.off('delete-file', this.deleteFile);
                events.off('delete-bucket', this.deleteBucket);
                events.off('delete-account', this.deleteAccount);
            },
            state: {
                active: false
            },
            getRandomNumber() {
                return Math.floor(Math.random() * Date.now())
            },
            confirmDeletion () {
                events.emit('open-modal-validation', { 
                    message: "Do you confirm you want to delete the File \"2024-04-invoice.pdf\"?",
                    callbackID: 'delete-file',
                    args: "file-" + this.getRandomNumber()
                });
            },
            confirmDeletionBucket() {
                events.emit('open-modal-validation', { 
                    message: "Are you sure you want to delete this S3 bucket \"2024-assets\"? This action is irreversible.",
                    callbackID: 'delete-bucket',
                    args: "bucket-" + this.getRandomNumber()
                });
            },
            confirmDeletionAccount() {
                events.emit('open-modal-validation', { 
                    message: "Are you sure you want to proceed with deleting your account? This action is irreversible and will permanently remove all your personal data associated with the account.",
                    callbackID: 'delete-account',
                    args: "account-" + this.getRandomNumber()
                });
            },
            deleteFile (id) {
                console.log("%c File deleted! ID: " + id, 'background: #222; color: #bada55');
            },
            deleteBucket (id) {
                console.log("%c Bucket deleted! ID: " + id, 'background: #222; color: #ff00ff');
            },
            deleteAccount (id) {
                console.log("%c Account deleted! ID: " + id, 'background: #222; color: #ff0000');
            }
        }
    </script>
</index-riot>
Enter fullscreen mode Exit fullscreen mode

Source Code: https://github.com/steevepay/riot-beercss/blob/main/examples/mitt/index.riot

Code breakdown:

  • On the onMounted Riot Hook, three Events are listening and redirecting to three function: deleteFile, deleteBucket, or deleteAccount. In real world production application, it could be an API call to delete a resource (file, or element on a Database)
  • Three buttons are created on the HTML, and each click emits the event open-modal-validation and passes a custom message, callback ID, and argument:
    1. The callbackID is an Event to emit when the validation button is clicked.
    2. The args is used to pass a resource ID from an API or Database, like a file ID or an Account ID. In our case, a random ID is passed as an argument.
  • When the Dialog is validated, it emits callbackID as an Event, and the corresponding function is executed to delete the file, bucket or account.
  • When the Component is removed from the web page, any event must be listened: When the onUnmounted Riot hook is executed, the event.off removes the Event subscription.

Conclusion

Instead of creating one Dialog for each button, only one Dialog is required for many buttons with the pub/sub pattern. Now, a component can listen to and emit events to communicate data based on actions. Voilà 🎉

Have a great day! Cheers 🍻

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .