# Interactivity ###### tags: `module-tutorial` `foundry-vtt` So far we've created a data layer, injected a non-functioning button, and created a FormApplication which only displays ToDos but does not edit them. It's time to bring it all together with some interactivity. ## I. Make the button open our `ToDoListConfig` There's two things we need to do to make `ToDoListConfig` openable. First we need to instanciate it, which is a fancy way of saying "create in memory." So far we've _defined_ `ToDoListConfig` but we haven't told the browser to actually _use_ it. ### `ToDoList.initialize` Back in our `ToDoList` class, create an `initialize` static method: ```js= static initialize() { this.toDoListConfig = new ToDoListConfig(); } ``` What we're doing here is creating a new static property of our `ToDoList` class named `toDoListConfig`. This is defined as a 'new' instance of our `ToDoListConfig` class. ### `init` There's a `Hook` that Foundry Core calls when it is ready for modules to start registering things like settings. We don't need settings yet, but we can use this `init` hook to run our own `initialize` method. Outside our `ToDoList` class, register an `init` hook callback: ```js= Hooks.once('init', () => { ToDoList.initialize(); }); ``` ### Make the button work Since `ToDoListConfig` is a subclass of `FormApplication`, we know that to make it appear, we need to call its [`render`](https://foundryvtt.com/api/FormApplication.html#render) method, with `true` as the first argument. We also set up `ToDoListConfig` to accept a second argument in the `render` method which can define which `userId`'s ToDo list to display. Inside our `renderPlayerList` hook callback we created an event listener which simply logs when a user clicks the button. Let's replace that log with something which A) gets the user row clicked on, and B) opens the ToDoListConfig with that user's list. #### A. Get the right userId We'll be using jQuery's [`parents`](https://api.jquery.com/parents/) method to grab the player list item's data-user-id, which is put there by Foundry Core. ```js= const userId = $(event.currentTarget).parents('[data-user-id]')?.data()?.userId; ``` This snippet takes the `currentTarget` (the button clicked), and goes up the DOM until it finds an element with `data-user-id` defined, then it gets that userId from the [`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) of the element. #### B. Render the instanciated ToDoListConfig ```js= ToDoList.toDoListConfig.render(true, {userId}); ``` Quite simply, take the `toDoListConfig` static property from our `ToDoList` class (which we defined during `init`), and call the `render` method, providing the userId we got in `A`. ### All together now! ```js= class ToDoList { // ... static initialize() { this.toDoListConfig = new ToDoListConfig(); } } // ... Hooks.once('init', () => { ToDoList.initialize(); }); // ... Hooks.on('renderPlayerList', (playerList, html) => { // ... // register an event listener for this button html.on('click', '.todo-list-icon-button', (event) => { const userId = $(event.currentTarget).parents('[data-user-id]')?.data()?.userId; ToDoList.toDoListConfig.render(true, {userId}); }); }); ``` <details> <summary>Success!</summary> ![Animated GIF showing the ToDo list opening and closing.](https://i.imgur.com/zUCTlLz.gif) </details> ## II. Make the Form Fields Edit ToDos Next we want to make our `input` elements actually persist the changes the user makes inside them. ### `ToDoListConfig.defaultOptions` There's two more overrides we should set with our ToDoListConfig's `defaultOptions` now that we're making it editable for real: ```js= closeOnSubmit: false, // do not close when submitted submitOnChange: true, // submit when any input changes ``` With these two options, our FormApplication will start trying to call its `_updateObject` method any time an `input` element is changed. This specifically happens on the [`blur`](https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event) event (when a user stops focusing) for text fields and on the [`change`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event) event for checkboxes. ### `ToDoListConfig#_updateObject` Overriding the `_updateObject` method is how we define what "saving" the form means. The FormApplication class we inherit from does a lot behind the scenes which helps transform user input into our data format automatically. ```js= async _updateObject(event, formData) { const expandedData = foundry.utils.expandObject(formData); await ToDoListData.updateUserToDos(this.options.userId, expandedData); this.render(); } ``` If that looks too easy, it's because we've done a lot before now to make it easy. Let's break that down. #### Argument 2: `formData` Go ahead and put a log into this method which prints `formData`: ```js= async _updateObject(event, formData) { ToDoList.log(false, 'saving', { formData }); ``` Refresh, Open the ToDoListConfig window and toggle a checkbox. In console you should see your log print an object that looks like the one below. > If you don't see any logs, make sure you have Developer Mode on, and you've turned on Debug Mode for "To Don't". > ```json= { "v09cveb05p8gpcl1.isDone": true, "v09cveb05p8gpcl1.label": "Dee", "yndgcuoq147g37nz.isDone": false, "yndgcuoq147g37nz.label": "kolasa", "wkqgdz8obom5pgzx.isDone": false, "wkqgdz8obom5pgzx.label": "goop", "gogdv4qvgydcr7sh.isDone": true, "gogdv4qvgydcr7sh.label": "goopa" } ``` This object has keys which correlate to the `name` of every `input` in our `form` element and values which are the values of those inputs at the time of being saved. This is useful, but we can make it even more useful with another Foundry Core helper: [`expandObject`](https://foundryvtt.com/api/module-helpers.html#.expandObject). #### `expandObject` Move the log below our definition for `expandedData` and include that in the output. ```js= async _updateObject(event, formData) { const expandedData = foundry.utils.expandObject(formData); ToDoList.log(false, 'saving', { formData, expandedData }); ``` Now when things log you should see those keys and values expanded out into an object whose shape should look familiar. ```json= { "v09cveb05p8gpcl1": { "isDone": true, "label": "Dee" }, "yndgcuoq147g37nz": { "isDone": false, "label": "kolasa" }, "wkqgdz8obom5pgzx": { "isDone": false, "label": "goopsdf" }, "gogdv4qvgydcr7sh": { "isDone": true, "label": "goopa" } } ``` That's exactly what our `ToDo`s look like when they're returned from `ToDoListData.getToDosForUser`. It also happens to be exactly what we need to give to `ToDoListData.updateUserToDos` to update a user's todos in bulk. #### Updating the ToDos Since we designed our Data Layer this way, our life is super easy here, we simply give our `ToDoListData.updateUserToDos` method the output of `foundry.utils.expandObject(formData)`. We also need to grab the `userId` off of the ToDoListConfig's `options`, which are convienently stored in `this.options`. Remember this is the id we defined when we opened the ToDoListConfig and that we used to populate the list in the first place. Put all those moving parts together and it "just works." Foundry's FormApplication is an extremely powerful tool if you get used to working with it. ### Test! To test that all of this works, refresh your page, open the ToDoListConfig, and then make some changes. We haven't wired up the Delete or Add buttons yet, so the changes should only be to the inputs. Once you've made changes close the ToDoListConfig and re-open it. If your changes persisted you're golden! ## III. Make the Buttons work Foundry Core's `FormApplication` has a lot of built-in logic to handle traditional form inputs, but when it comes to other interactive elements like our add and delete `button`s, we have to do the work ourselves. We need to attach some more event listeners to our buttons. ### `activateListeners` The `FormApplication#activateListeners` method is intended to be overriden with our subclass's behavior. We'll again use jQuery's [`on`](https://api.jquery.com/on/) method to do register our event listeners to make our lives easier here. ```js= activateListeners(html) { html.on('click', "[data-action]", this._handleButtonClick); } ``` We're going start by attaching an event listener which triggers a seperately defined callback method when any element with a `data-action` attribute is clicked. ### `_handleButtonClick` To keep things tidy in our `activateListeners` method we're going to define a new method on our `ToDoListConfig` class which is where we'll put the callback to the button click event listener. ```js= async _handleButtonClick(event) { const clickedElement = $(event.currentTarget); const action = clickedElement.data().action; const toDoId = clickedElement.parents('[data-todo-id]')?.data()?.todoId; ToDoList.log(false, 'Button Clicked!', {action, toDoId}); } ``` :::info **Programming Concept: Async** Some of the things we'll be doing in the upcoming steps require the ability to wait for Foundry's backend to finish before moving on. To allow that we'll be using `async` and `await`. These are a convienent way to interact with javascript Promises, which are used extensively when dealing with operations that take time like a network request. This is a huge topic with [very in depth guides](https://www.freecodecamp.org/news/javascript-async-await-tutorial-learn-callbacks-promises-async-await-by-making-icecream/) out there on the internet if you'd like more details. ::: From the `event` argument passed to our callback, we can get the element that was clicked with [`event.currentTarget`](https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget). This target element should have a [`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) on it, which in turn should have the `action` we defined with `data-action=""` in our Handlebars Template. Additionally, one of the parents of the clicked element might have a data attribute which denotes the id of the relevant ToDo (the delete button). ### Test! We set `data-action="create"` and `data-action="delete"` for the add and the delete button respectively. So when we click on those buttons, we should see that being logged appropriately. ![Image showing the console for Buttons being clicked.](https://i.imgur.com/qM00zB8.png) Perfect! But there's a gotchya here. Test if the form inputs are still persisting. We didn't make any changes to the stuff we did before, so they should right? > ***Narrator:*** They do not. ### Debugging Form Inputs that Don't Save Even though none of the code we wrote to make the form inputs work correctly was touched, we did affect that code-path in a subtle way. By overriding `activateListeners` we disabled all of Foundry's built-in listeners that save the form when an input changes. We want those listeners but we also want our own, so we need to use `super` again. ```js= activateListeners(html) { super.activateListeners(html); html.on('click', "[data-action]", this._handleButtonClick); } ``` With `super.activateListeners` we are ensuring that the inherited method is still being used, but also adding some extras afterwards. This is a super common mistake to make and one of the first things you should check if your form inputs worked before, but suddenly stopped working recently. ### Using the `action` Our `activateListeners` now has an event handler which knows which button was clicked, it's time to make each kind of button click do the right thing. We can use a [`switch` statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch) to do this efficiently. ```js= switch (action) { case 'create': { await ToDoListData.createToDo(this.options.userId); this.render(); break; } case 'delete': { await ToDoListData.deleteToDo(toDoId); this.render(); break; } default: ToDoList.log(false, 'Invalid action detected', action); } ``` A lot is going on here so let's break it down one case at a time. #### `case 'create'` When the `action` for the clicked button is `'create'`, we want to: 1. Use our `ToDoListData.createToDo` method to create a blank new ToDo on the user whose list we are looking at. 2. When that is done, Re-render the form. 3. `break`, which prevents the rest of our `switch` from running. The `await` is important here because without it, we would re-render the form before the Foundry Backend was done creating our ToDo. #### `case 'delete'` Similar to `create`, when the `action` for the button is `'delete'` we want to: 1. Use our `ToDoListData.deleteToDo` method to delete the appropriate ToDo. 2. When that is done, Re-render the form. 3. `break`, which prevents the rest of our `switch` from running. Again the `await` is important because without it, we would re-render the form before the ToDo was done being deleted. #### `default` In case by some stroke of bad luck something other than `create` or `delete` was to trigger this flow, we want to do nothing (and log something). ### Test! You know the drill, refresh, open the ToDoListConfig and click the "Add To-Do" button. We should see a new blank ToDo pop into existence at the end of the list. > ***Narrator:*** We do not. :::danger Uncaught (in promise) TypeError: Cannot read property 'userId' of undefined ::: If you were particularly adventurous and tried deleting a ToDo instead of adding one, you'd see a similar error complaining about `render` instead of `userId`. ### Debugging `Cannot read property X of undefined` This is a very common issue to run into and it happens when we tell javascript to read a property of an object which does not exist. ##### Example: ```js= const foo = undefined; foo.bar(); // TypeError: Cannot read property 'bar' of undefined ``` In this particular case we are trying to read the `userId` property of something which isn't defined. ![Image of the Error](https://i.imgur.com/menNLiN.png) The console gives us some useful information to help debug this. - What function is erroring? `HTMLButtonElement._handleButtonClick` - Which file is erroring? `todo-list.js` - What line of that file is erroring? `:192` So taking a look at our file we can tell that this is what's tripping: ```js= await ToDoListData.createToDo(this.options.userId); ``` To cause this error, `this.options` must be `undefined`. That's wierd, we don't even define `this.options` because it's part of Foundry Core's `FormApplication` logic. We also know it works because we use `this.options.userId` in other methods. Let's try adding `this` to our log in the `_handleButtonClick` method. ```js= ToDoList.log(false, 'Button Clicked!', { this: this, action, toDoId }); ``` ![Image of the console log.](https://i.imgur.com/lsXAhKa.png) We expect `this` to be an instance of `ToDoListConfig`, but for some reason it's an HTML element `button`. #### jQuery messes with `this` We used jQuery to make our lives easier when we made our event listener. The `on` method jQuery provides overrides `this` in the provided callback to be the element clicked. That's useful for a lot of reasons, but not what we want here. To fix this, we need to explicitly `bind` `this` when we give `on` its callback. ```js= html.on('click', "[data-action]", this._handleButtonClick.bind(this)); ``` Doing so ensures that `this` remains the ToDoListConfig in the callback method. #### Once more, with feeling! ![Image displaying the new log of button click.](https://i.imgur.com/7F4f6GI.png) <details> <summary>Success!</summary> ![Animated GIF showing items in the ToDo list being created and deleted.](https://i.imgur.com/um9BQIV.gif) </details> ## V. Wrapping Up We accomplished a lot in this section: - Initialized our ToDoListConfig - Made our injected button open said ToDoListConfig window - Learned about `FormApplication._updateObject` and how to make data persist on edit - Made our form fields persist edited data - Learned about `FormApplication.activateListeners` and how to use it to make buttons work - Debugged why a custom `activateListeners` method stopped our data persistence - Learned how to debug `Cannot read property X of undefined` errors - Made our form's buttons function <details> <summary>Final ToDoList `initialize`</summary> ```js= static initialize() { this.toDoListConfig = new ToDoListConfig(); } ``` </details> <details> <summary>Final init hook</summary> ```js= /** * Once the game has initialized, set up our module */ Hooks.once('init', () => { ToDoList.initialize(); }); ``` </details> <details> <summary>Final ToDoListConfig</summary> ```js= class ToDoListConfig extends FormApplication { static get defaultOptions() { const defaults = super.defaultOptions; const overrides = { closeOnSubmit: false, height: 'auto', id: 'todo-list', submitOnChange: true, template: ToDoList.TEMPLATES.TODOLIST, title: 'To Do List', userId: game.userId, }; const mergedOptions = foundry.utils.mergeObject(defaults, overrides); return mergedOptions; } async _handleButtonClick(event) { const clickedElement = $(event.currentTarget); const action = clickedElement.data().action; const toDoId = clickedElement.parents('[data-todo-id]')?.data()?.todoId; switch (action) { case 'create': { await ToDoListData.createToDo(this.options.userId); this.render(); break; } case 'delete': { await ToDoListData.deleteToDo(toDoId); this.render(); break; } default: ToDoList.log(false, 'Invalid action detected', action); } } activateListeners(html) { super.activateListeners(html); html.on('click', "[data-action]", this._handleButtonClick.bind(this)); } getData(options) { return { todos: ToDoListData.getToDosForUser(options.userId) } } async _updateObject(event, formData) { const expandedData = foundry.utils.expandObject(formData); await ToDoListData.updateUserToDos(this.options.userId, expandedData); } } ``` </details> <details> <summary>Final `renderPlayerList` hook</summary> ```js= Hooks.on('renderPlayerList', (playerList, html) => { // find the element which has our logged in user's id const loggedInUserListItem = html.find(`[data-user-id="${game.userId}"]`) // create localized tooltip const tooltip = game.i18n.localize('TODO-LIST.button-title'); // insert a button at the end of this element loggedInUserListItem.append( `<button type='button' class='todo-list-icon-button flex0' title="${tooltip}"> <i class='fas fa-tasks'></i> </button>` ); // register an event listener for this button html.on('click', '.todo-list-icon-button', (event) => { const userId = $(event.currentTarget).parents('[data-user-id]')?.data()?.userId; ToDoList.toDoListConfig.render(true, { userId }); }); }); ``` </details> Next Step: [Finishing Touches](/HyIkG82nTfOI-gcaxip3yQ) {%hackmd io_aG7zdTKyRe3cpLcsxzw %}