-2

This is my first time using javascript classes . I am using classes to return an html form . When the form is submitted I want to show an alert box that says " "submitted" . I have a problem doing this with classes as my submit function is not recognised .

export default class Contact extends AbstractView {
    
  constructor(){
    super();
    this.setTitle("Contact Us");
  }

  test(e){
    e.preventDefault();
    alert('submitted');
  }

  async getHTML(){
     return ` 
       <form method = "POST" onsubmit = "test(this)">
          <h2  > Send Message </h2>
          <div class = "inputBox">
            <input type = "text" name = "" required = "required" />
            <span> Full Name  </span>
          </div>
          <div class = "inputBox">
            <input type = "submit" name = "" value="Send"/>
          </div>
        </form>`;
  }


}

I want this test() function specifically for my contact object so I did not initialize it in my constructor .

The form is submitted with no alert shown and I get no error .

I am using classes to return html code for a single page app using vanilla js .

My index.js handles this functionality based on the url hash and appends the html from getHtml() to a <div>

index.js

import DashBoard from './views/DashBoard.js';
import Posts from './views/Posts.js';
import Settings from './views/Settings.js';
import Contact from './views/Contact.js';



window.addEventListener("hashchange",()=>Router() );

document.addEventListener("DOMContentLoaded" , ()=>{
  document.body.addEventListener('click',(e)=>{
    if(e.target.matches("[data-link]")){
      e.preventDefault();
      navigateTo(e.target.href);
    }
  });
  Router();
});

function navigateTo(url){
  history.pushState(null,null,url);
  Router();
}


async function Router (){
  const routes = [
    {path: "#" , view:DashBoard},
    {path: "#posts" , view:Posts},
    {path: "#settings" , view:Settings},
    {path:"#contact", view:Contact},
  ];

  const potentialMatches = routes.map((route)=>{
    return {route:route , isMatch:location.hash===route.path}
  });
  
  let match = potentialMatches.find(potentialMatch=>potentialMatch.isMatch);
  if(!match){

    match = {
      route:routes[0],
      isMatch:true,
    }

  }
  const view = new match.route.view();
  let content = await view.getHtml();
  document.querySelector('#app').innerHTML = content;

};
  • 1
    The problem isn't in the code you've shown, it's in the code calling the method that has `this.test()` in it. In that method, `this` isn't what you think it is, probably because of the issues described in the [linked question](https://stackoverflow.com/questions/20279484/). If those don't seem to apply to your situation, though, please update your question with a [mcve] demonstrating the problem, ideally a **runnable** one using Stack Snippets (the `[<>]` toolbar button); [here's how to do one](https://meta.stackoverflow.com/questions/358992/). @ notify me and I'll look at the edit. – T.J. Crowder May 12 '21 at 08:37
  • @T.J.Crowder This link has a huge example which does not cover calling a function in an html element without the "this" keyword – Βαρβάρα Ξιάρχου May 12 '21 at 08:43
  • 1
    It's **the canonical question and answers** for the problem you're likely having. But again, feel free to edit the question with a [mcve] for further help. Also, please remember that the people who help you on Stack Overflow are doing so on their own time and out of the goodness of their hearts. – T.J. Crowder May 12 '21 at 08:45
  • @T.J.Crowder I just edited my question . You can check it if you want – Βαρβάρα Ξιάρχου May 12 '21 at 08:53
  • You've completely changed the question. Originally you said *"I get the error Uncaught TypeError: this.test is not a function ."* and how you say you don't get any error (and the code you've shown certainly wouldn't give you that error). But with the change, it's no longer a duplicate of the previous linked question... – T.J. Crowder May 12 '21 at 08:56
  • @T.J.Crowder Yes the principles just seemed the same for a smaller example – Βαρβάρα Ξιάρχου May 12 '21 at 08:58
  • We can't help you without knowing what is calling your `getHTML` function and putting that HTML in the DOM. Assuming some code you haven't shown does do that, that HTML will make the browser try to call a global `test` function when submitting the form. I'm surprised there's no error, b/c it seems unlikely you have a global `test` function. You can't just return HTML and have something render it and later have the submit function call `test` on your `Contact` object unless the thing putting your HTML in the DOM does something to modify it for you. So again, we can't help without knowing more. – T.J. Crowder May 12 '21 at 09:02
  • So at the risk of repeating myself a third time, please update your question with a [mcve] demonstrating the problem, ideally a **runnable** one using Stack Snippets (the `[<>]` toolbar button); [here's how to do one](https://meta.stackoverflow.com/questions/358992/). – T.J. Crowder May 12 '21 at 09:02
  • 1
    Passing HTML as text around within Javascript isn't a good idea. It's extremely limiting, and opens all sorts of issues around text escaping. You should be using the DOM API to construct DOM objects to which you programmatically add event listeners; doing so you can pass function references and objects directly, instead of trying to pass the name of some function as text. – deceze May 12 '21 at 09:05
  • @T.J.Crowder This is my full code example . I am using vanilla js to create a single page application . getHTML() is a class method that appends html to a div based on the url of the page . In our case with Contact I have a contact form with a submit method . Thank you in advance – Βαρβάρα Ξιάρχου May 12 '21 at 09:07

1 Answers1

2

You've shown that you're trying to use the result of getHtml like this:

let content = await view.getHtml();
document.querySelector('#app').innerHTML = content;

(NOTE: In your Contact code, it's getHTML, not getHtml; they need to match, including capitalization.)

That means that onsubmit = "test(this)" in your HTML will try to call a global test function, passing in the form element as the first argument. But your test method isn't a global function, it's a method of an object (presumably one created via new Contact). Also, it's expecting its first argument to be an event object, not a form element.

To make this work, you need to make the DOM event handler call the test method of that specific object, which you can't do with onxyz-attribute-style event handlers. They can only call global functions.

You could use code that post-processes elements to attach event handlers to them. For instance, suppose instead of onSubmit="test(this)" you did data-events="submit:test". Then the code doing the rendering could hook those up for you:

const content = await view.getHTML();
const app = document.querySelector("#app");
app.innerHTML = content;
// Find the elements we need to handle
const elements = app.querySelectorAll("[data-events]");
for (const element of elements) {
    // Get the events to hook up on this element
    const events = element.getAttribute("data-events").split(",");
    for (const event of events) {
        // Get the event name and method name
        const [eventName, methodName] = event.split(":");
        // Hook up the handler, using `Function.prototype.bind` to ensure that
        // `this` during the call refers to `view`.
        element.addEventListener(eventName, view[methodName].bind(view));
    }
    // (You could use `removeAttribute` here to remove the `data-events` attribute if you liked
}

Your method would then get called with this referring to the Contact instance you got the HTML from. The first argument will be an event object. You can get the form element from it via event.currentTarget though:

test(event) {
    const form = event.currentTarget;
    // ...
}

Live Example:

class Contact /*extends AbstractView*/ {
    title = "";

    constructor(){
        // super();
        this.title = "Contact Us";
    }

    test(event) {
        event.preventDefault();
        console.log(`Submitted; this.title = ${this.title}`);
    }

    async getHTML(){
        return ` 
            <form method = "POST" data-events = "submit:test">
                <h2> Send Message </h2>
                <div class = "inputBox">
                    <input type = "text" name = "" required = "required" />
                    <span> Full Name  </span>
                </div>
                <div class = "inputBox">
                    <input type = "submit" name = "" value="Send"/>
                </div>
            </form>`;
    }
}

(async () => {
    const view = new Contact();

    const content = await view.getHTML();
    const app = document.querySelector("#app");
    app.innerHTML = content;
    // Find the elements we need to handle
    const elements = app.querySelectorAll("[data-events]");
    for (const element of elements) {
        // Get the events to hook up on this element
        const events = element.getAttribute("data-events").split(",");
        for (const event of events) {
            // Get the event name and method name
            const [eventName, methodName] = event.split(":");
            // Hook up the handler, using `Function.prototype.bind` to ensure that
            // `this` during the call refers to `view`.
            element.addEventListener(eventName, view[methodName].bind(view));
        }
        // (You could use `removeAttribute` here to remove the `data-events` attribute if you liked
    }
})();
<div id="app"></div>

You seem to be trying to create your own DOM library framework or similar. You'd probably be better off just writing normal HTML and hooking up handlers via document.getElementById, document.querySelector, document.querySelectorAll, using event delegation, etc.; or using one of the many, many tried-and-test DOM library frameworks.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875