简体   繁体   中英

Create HTML using javascript with context/variables

I want to create HTML from stored objects using javascript. They include buttons with onclick functions which call a function inside the object.

I tried creating every node using document.createElement("Div") and setting its content/classes... but found this solution to be very clunky.

Currently I am setting the innerHTML of my node to a string I created, which is way more readable.

let objHTML = document.createElement("Div");
objHTML.innerHTML = 
`<div class="ObjWrap ${status}">
    <p class="objName">${name}</p>
    <p class="objContent">${content}</p>
    <button class="objEdit" onclick="${edit(evt)}">e</button>
    <button class="objActive" onclick="${activity(evt)}">x</button>
</div>`;

The edit and activity functions are local and access variables of the object, so I cant write "onclick="edit(evt)"... - but my solution also doesnt work, i guess the edit and activity functions are just evaluated and being put in the html. I could use eventlisteners instead of the onclick events on the buttons, but this would again not be as readable and I would also have to access the button again to attach the eventlistener (in this case something like objHTML.getElementsByTagName('div')[0].getElementsByTagName('div')[0].getElementsByTagName('button ')[0/1]).

My question is how can I create my HTML while keeping it as readable as it is and allowing me to add onclick functions which reference a variable to a button/any element. I cant use Ids since these elements are being created multiple times.

Edit: Codepen to show what I mean, cut out of context so some things might not make sence: https://codepen.io/anon/pen/jdEqwQ

What you're looking for is a view framework, I believe. Basically, a library that does all this funny databinding business, data conversion, and whatnot.

There is a whole bunch of them , but the most popular and supported ones nowadays are React , Angular , and Vue .

They will take care of such data-binding, as well as ensuring values stay synchronized between code and UI, as well as efficient reuse of elements whenever possible. Each of them has different workflows, you'll want to do some research to find the one you think is the best fit for your use case and knowledge.

If your application is small, you maybe don't need to use them, but if you're feeling this kind of pain already, do consider, they are very well-built tools.

Is it an option to define the functions that you want your buttons to execute?

This would create the div you're looking for and add the handler for a defined function to it.

function test() { console.log('test') };
let obj = document.createElement("Div");
obj.innerHTML = '<div>test <button onclick="test()">run</button></div>';
document.body.appendChild(obj)

But other than that, I would agree with the comments that indicate using a framework like vue.

Edit: Does something like this go in the direction that you're expecting to see?

function generate() {
    function test() {
        return function(e) {
            console.log('event', e);
        }
    };

    const btn1 = document.createElement('button');
    btn1.onclick = test();
    btn1.innerHTML = 'Run'


    const obj = document.createElement("Div");


    obj.innerHTML = '<div>test<br></div>';
    obj.appendChild(btn1);
    document.body.appendChild(obj)
}

generate()

I doubt it is possible to get a function reference in there as string that is not in the global scope. This goes against your clean approach of writing a HTML template, though.

This isn't that hard to create actually. It helps to make utility functions to take care of the heavy lifting:

let create = (tag, opts = {}) => (Object.assign(document.createElement(tag), opts)),
nested = (p, c) =>(c) ?(p.appendChild(c), (c2) => nested.call(p, p, c2)) : p;

All create does is allow you to create elements with attributes and properties rather easily and with greater speed.

All nested does is place the nodes within the original container one after the other. The container is the first element within nested, the first child is the second, etc.

Then we load in some dummy data:

//your template stuff
let template=new Map([["status","online"],["name","Zak"],["content","This is some 
content"],["edit",function(){alert("editing!")}],["activity",function(){alert("some 
activity!")}]]);

And put it together:

let objHTML = nested(create("DIV", {
  className: "container"
}), create("p", {
  className: "objName"
}))(create("button", {
  className: "objEdit",
  onclick: template.get("edit"),
  textContent: "edit!"
}))(create("button", {
  className: "objContent",
  textContent: template.get("content"),
  onclick: () => alert("clicking does nothing... oh wait")
}))(create("button", {
  className: "objActive",
  onclick: template.get("activity"),
  textContent: "activity"
}))();

voila. That's pretty much it, and obviously we can do much more given the above two functions alone.

Here's a demo of your above code( just a quick glance at it anyway )

 let create = (tag, opts = {}) => (Object.assign(document.createElement(tag), opts)), nested = (p, c) => (c) ?(p.appendChild(c), (c2) => nested.call(p, p, c2)) : p; //your template stuff let template=new Map([["status","online"],["name","Zak"],["content","This is some content"],["edit",function(){alert("editing!")}],["activity",function(){alert("some activity!")}]]); let objHTML = nested( create("DIV", { className: "container" }), create("p", { className: "objName" }))(create("button", { className: "objEdit", onclick: template.get("edit"), textContent: "edit!" }))(create("button", { className: "objContent", textContent: template.get("content"), onclick: ()=>alert("clicking does nothing... oh wait"), }))(create("button", { className: "objActive", onclick: template.get("activity"), textContent: "activity" }) )(); document.body.appendChild(objHTML); console.log(objHTML); 

I do agree with others that there are plenty of libraries that provide services for these types of things, but it's important to realize that many of them are not difficult enough to require pulling in a large, external resource when you can just roll your own - given, of course, that the task is small enough and you don't require a ton of extra bells and whistles.

Note : Just as an aside, React without a JSX transpiler looks remarkably like this code - though as I've stated above React still has access to many things that you would have to put a great deal of time into to recreate - just don't sell yourself short. It's good to get into the underpinnings and check out the gears sometimes! Happy Coding!

If you're learning JavaScript, and you don't want to bother with new libraries or frameworks, you could just use vanilla JS classes. It would mean you setting up the methods to deal with the listeners rather than having them in the HTML, but it's cleaner.

This small example is not perfect, but it might give you an idea what you can do. It's deliberately made to resemble React's functionality should you decide to eventually move to that at some point.

 class Component { constructor(props, i) { // Assign the component index as `id` this.state = { id: i }; // Loop over the passed in data and add them // to the class context for (let [p, v] of Object.entries(props)) { this.state[p] = v; }; // Bind the edit handler so that the context sticks this.handleEdit = this.handleEdit.bind(this); // Now create the wrapper element, render the HTML // and set the listeners this.addElement(); this.render(); this.setListeners(); } // `querySelector` and `addEventListener` shortcut // Takes a selector and a function to call when the button // is clicked qsl(s, fn) { const el = document.querySelector(s); return el.addEventListener('click', fn, false); } // set the button listeners for HTML corresponding // to this component id setListeners() { const { id } = this.state; const selector = `[data-id="${id}"]`; this.qsl(`${selector} .edit`, this.handleEdit); } // An example based on your code - toggle // the status and then re-render the HTML handleEdit() { const { status } = this.state; this.state.status = !status; this.render(); } // Adds the wrapper element to the DOM and caches it addElement() { const { id } = this.state; const html = `<div data-id="${id}" class="component"></div>`; document.body.insertAdjacentHTML('beforeend', html); this.wrapper = document.querySelector(`[data-id="${id}"].component`); } // return your compiled template getHTML() { const { id, status, name } = this.state; return `<div data-id="${id}"><p>${status} - ${name}</p><button class="edit">e</button></div>`; } // Render the output. // Sadly, because we're not using a virtual DOM (like React // for example) we're destroying the existing HTML and replacing it // so we need to re-add the event listeners to that new markup render() { this.wrapper.innerHTML = this.getHTML(); this.setListeners(); } } // Set up the data for the new objects const data = [ { status: false, name: 'Bob', content: '' }, { status: true, name: 'Mick', content: '' } ]; // Loop over the data to create the components // We pass in the index to the class as it makes for // a good id const components = data.map((component, i) => { return new Component(component, i); }); 
 .component { border: 1px solid #454545; width: 50%; padding: 0.2em; margin-bottom: 0.1em; } 
 <div class="wrapper"></div> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM