Andrew Krigline
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 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 %}

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully