用普通的旧JS动态渲染DOM元素的好方法是什么?
What's a good way to dynamically render DOM elements with plain old JS?
我面临的挑战是用普通的旧Javascript构建一个单页应用程序,不允许使用库或框架。虽然在React和Angular中创建动态DOM元素相当简单,但我提出的香草JS解决方案似乎很笨拙。我想知道是否有一种特别简洁或有效的方式来构建动态渲染的DOM元素?
下面的函数接受从GET请求接收的数组,并为每个项目呈现一个div,传递值(很像您在React和呈现子元素中映射结果)。
function loadResults(array) {
array.forEach(videoObject => {
let videoData = videoObject.snippet;
let video = {
title : videoData.title,
img : videoData.thumbnails.default.url,
description : videoData.description
};
let div = document.createElement("DIV");
let img = document.createElement("IMG");
img.src = video.img;
let h4 = document.createElement("h4");
let title = document.createTextNode(video.title);
h4.appendChild(title);
let p = document.createElement("p");
let desc = document.createTextNode(video.description);
p.appendChild(desc);
div.appendChild(img);
div.appendChild(h4);
div.appendChild(p);
document.getElementById('results')
.appendChild(div);
});
}
这感觉不必要的笨拙,但我还没有找到一个更简单的方法来做到这一点。
提前感谢!
一个好的方法是创建一个函数来为你创建元素。像这样:注意:我在这里所说的一切都是在概念证明层面上的,仅此而已。它不处理错误或异常情况,过去也是如此它在生产中进行了测试。请自行决定。
const crEl = (tagName, attributes = {}, text) => {
const el = document.createElement(tagName);
Object.assign(el, attributes);
if (text) { el.appendChild(document.createTextNode(text)); }
return el;
};
那么你可以这样使用:
results
.map(item => crEl(div, whateverAttributes, item.text))
.forEach(el => someParentElement.appendChild(el));
我看到的另一个很酷的概念证明是使用ES6代理作为一种模板引擎。const t = new Proxy({}, {
get(target, property, receiver) {
return (children, attrs) => {
const el = document.createElement(property);
for (let attr in attrs) {
el.setAttribute(attr, attrs[attr]);
}
for (let child of(Array.isArray(children) ? children : [children])) {
el.appendChild(typeof child === "string" ? document.createTextNode(child) : child);
}
return el;
}
}
})
const el = t.div([
t.span(
["Hello ", t.b("world!")], {
style: "background: red;"
}
)
])
document.body.appendChild(el);
代理在目标对象上捕获get
(它是空的),并呈现一个具有被调用方法名称的元素。这就产生了const el =
中非常酷的语法。
如果你可以使用ES6,模板字符串是另一个想法->
var vids = [
{
snippet: {
description: 'hello',
title: 'test',
img: '#',
thumbnails: { default: {url: 'http://placehold.it/64x64'} }
}
}
];
function loadResults(array) {
array.forEach(videoObject => {
let videoData = videoObject.snippet;
let video = {
title : videoData.title,
img : videoData.thumbnails.default.url,
description : videoData.description
};
document.getElementById('results').innerHTML = `
<div>
<img src="${video.img}"/>
<h4>${video.title}</h4>
<p>${video.description}</p>
</div>
`;
});
}
loadResults(vids);
<div id="results"></div>
在我看来,如果您不使用任何模板引擎,您希望尽可能多地控制如何将元素组合在一起。因此,合理的方法是抽象常见任务,并允许链接调用以避免额外的变量。所以我想这样写(不是很花哨):
function CE(el, target){
let ne = document.createElement(el);
if( target )
target.appendChild(ne);
return ne;
}
function CT(content, target){
let ne = document.createTextNode(content);
target.appendChild(ne);
return ne;
}
function loadResults(array) {
var results = document.getElementById('results');
array.forEach(videoObject => {
let videoData = videoObject.snippet;
let video = {
title : videoData.title,
img : videoData.thumbnails.default.url,
description : videoData.description
};
let div = CE('div');
let img = CE("IMG", div);
img.src = video.img;
CT(video.title, CE("H4", div));
CT(video.description, CE("p", div););
results.appendChild(div);
});
}
你得到的是你仍然可以很好地控制你的元素是如何组装的,什么连接到什么。但是你的代码更容易理解
出于同样的目的,我创建了一个库,您可以在https://www.npmjs.com/package/object-to-html-renderer找到它我知道你不能在你的项目中使用任何库,但因为这个很短(没有依赖关系),你可以只是复制和适应代码,这只是一个小文件:(见https://gitlab.com/kuadrado-software/object-to-html-renderer/-/blob/master/index.js)
// object-to-html-renderer/index.js
module.exports = {
register_key: "objectToHtmlRender",
/**
* Register "this" as a window scope accessible variable named by the given key, or default.
* @param {String} key
*/
register(key) {
const register_key = key || this.register_key;
window[register_key] = this;
},
/**
* This must be called before any other method in order to initialize the lib.
* It provides the root of the rendering cycle as a Javascript object.
* @param {Object} renderCycleRoot A JS component with a render method.
*/
setRenderCycleRoot(renderCycleRoot) {
this.renderCycleRoot = renderCycleRoot;
},
event_name: "objtohtml-render-cycle",
/**
* Set a custom event name for the event that is trigger on render cycle.
* Default is "objtohtml-render-cycle".
* @param {String} evt_name
*/
setEventName(evt_name) {
this.event_name = evt_name;
},
/**
* This is the core agorithm that read an javascript Object and convert it into an HTML element.
* @param {Object} obj The object representing the html element must be formatted like:
* {
* tag: String // The name of the html tag, Any valid html tag should work. div, section, br, ul, li...
* xmlns: String // This can replace the tag key if the element is an element with a namespace URI, for example an <svg> tag.
* See https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS for more information
* style_rules: Object // a object providing css attributes. The attributes names must be in JS syntax,
* like maxHeight: "500px", backgrouncColor: "#ff2d56", margin: 0, etc.
* contents: Array or String // This reprensents the contents that will be nested in the created html element.
* <div>{contents}</div>
* The contents can be an array of other objects reprenting elements (with tag, contents, etc)
* or it can be a simple string.
* // All other attributes will be parsed as html attributes. They can be anything like onclick, href, onchange, title...
* // or they can also define custom html5 attributes, like data, my_custom_attr or anything.
* }
* @returns {HTMLElement} The output html node.
*/
objectToHtml(obj) {
if (!obj) return document.createElement("span"); // in case of invalid input, don't block the whole process.
const objectToHtml = this.objectToHtml.bind(this);
const { tag, xmlns } = obj;
const node = xmlns !== undefined ? document.createElementNS(xmlns, tag) : document.createElement(tag);
const excludeKeys = ["tag", "contents", "style_rules", "state", "xmlns"];
Object.keys(obj)
.filter(attr => !excludeKeys.includes(attr))
.forEach(attr => {
switch (attr) {
case "class":
node.classList.add(...obj[attr].split(" ").filter(s => s !== ""));
break;
case "on_render":
if (!obj.id) {
node.id = `${btoa(JSON.stringify(obj).slice(0, 127)).replace(/'=/g, '')}${window.performance.now()}`;
}
if (typeof obj.on_render !== "function") {
console.error("The on_render attribute must be a function")
} else {
this.attach_on_render_callback(node, obj.on_render);
}
break;
default:
if (xmlns !== undefined) {
node.setAttributeNS(null, attr, obj[attr])
} else {
node[attr] = obj[attr];
}
}
});
if (obj.contents && typeof obj.contents === "string") {
node.innerHTML = obj.contents;
} else {
obj.contents &&
obj.contents.length > 0 &&
obj.contents.forEach(el => {
switch (typeof el) {
case "string":
node.innerHTML = el;
break;
case "object":
if (xmlns !== undefined) {
el = Object.assign(el, { xmlns })
}
node.appendChild(objectToHtml(el));
break;
}
});
}
if (obj.style_rules) {
Object.keys(obj.style_rules).forEach(rule => {
node.style[rule] = obj.style_rules[rule];
});
}
return node;
},
on_render_callbacks: [],
/**
* This is called if the on_render attribute of a component is set.
* @param {HTMLElement} node The created html element
* @param {Function} callback The callback defined in the js component to render
*/
attach_on_render_callback(node, callback) {
const callback_handler = {
callback: e => {
if (e.detail.outputNode === node || e.detail.outputNode.querySelector(`#${node.id}`)) {
callback(node);
const handler_index = this.on_render_callbacks.indexOf((this.on_render_callbacks.find(cb => cb.node === node)));
if (handler_index === -1) {
console.warn("A callback was registered for node with id " + node.id + " but callback handler is undefined.")
} else {
window.removeEventListener(this.event_name, this.on_render_callbacks[handler_index].callback)
this.on_render_callbacks.splice(handler_index, 1);
}
}
},
node,
};
const len = this.on_render_callbacks.push(callback_handler);
window.addEventListener(this.event_name, this.on_render_callbacks[len - 1].callback);
},
/**
* If a main element exists in the html document, it will be used as rendering root.
* If not, it will be created and inserted.
*/
renderCycle: function () {
const main_elmt = document.getElementsByTagName("main")[0] || (function () {
const created_main = document.createElement("main");
document.body.appendChild(created_main);
return created_main;
})();
this.subRender(this.renderCycleRoot.render(), main_elmt, { mode: "replace" });
},
/**
* This method behaves like the renderCycle() method, but rather that starting the rendering cycle from the root component,
* it can start from any component of the tree. The root component must be given as the first argument, the second argument be
* be a valid html element in the dom and will be used as the insertion target.
* @param {Object} object An object providing a render method returning an object representation of the html to insert
* @param {HTMLElement} htmlNode The htlm element to update
* @param {Object} options can be used the define the insertion mode, default is set to "append" and can be set to "override",
* "insert-before" (must be defined along with an insertIndex key (integer)),
* "adjacent" (must be defined along with an insertLocation key (String)), "replace" or "remove".
* In case of "remove", the first argument "object" is not used and can be set to null, undefined or {}...
*/
subRender(object, htmlNode, options = { mode: "append" }) {
let outputNode = null;
const get_insert = () => {
outputNode = this.objectToHtml(object);
return outputNode;
};
switch (options.mode) {
case "append":
htmlNode.appendChild(get_insert());
break;
case "override":
htmlNode.innerHTML = "";
htmlNode.appendChild(get_insert());
break;
case "insert-before":
htmlNode.insertBefore(get_insert(), htmlNode.childNodes[options.insertIndex]);
break;
case "adjacent":
/**
* options.insertLocation must be one of:
*
* afterbegin
* afterend
* beforebegin
* beforeend
*/
htmlNode.insertAdjacentElement(options.insertLocation, get_insert());
break;
case "replace":
htmlNode.parentNode.replaceChild(get_insert(), htmlNode);
break;
case "remove":
htmlNode.remove();
break;
}
const evt_name = this.event_name;
const event = new CustomEvent(evt_name, {
detail: {
inputObject: object,
outputNode,
insertOptions: options,
targetNode: htmlNode,
}
});
window.dispatchEvent(event);
},
};
这是整个库,它可以这样使用(还有更多的功能,但至少是基本用法):
// EXAMPLE - refresh a list after fetch data
const renderer = require("object-to-html-renderer");
class DataListComponent {
constructor() {
this.render_data = [];
this.list_id = "my-data-list";
}
async fetchData() {
const fetchData = await (await fetch(`some/json/data/url`)).json();
return fetchData;
}
renderDataItem(item) {
return {
tag: "div",
contents: [
// Whatever you want to do to render your data item...
],
};
}
renderDataList() {
return {
tag: "ul",
id: this.list_id,
contents: this.render_data.map(data_item => {
return {
tag: "li",
contents: [this.renderDataItem(data_item)],
};
}),
};
}
render() {
return {
tag: "div",
contents: [
{
tag: "button",
contents: "fetch some data !",
onclick: async () => {
const data = await this.fetchData();
this.render_data = data;
renderer.subRender(
this.renderDataList(),
document.getElementById(this.list_id),
{ mode: "replace" },
);
},
},
this.renderDataList(),
],
};
}
}
class RootComponent {
render() {
return {
tag: "main", // the tag for the root component must be <main>
contents: [new DataListComponent().render()],
};
}
}
renderer.setRenderCycleRoot(new RootComponent());
renderer.renderCycle();
我用这个构建了整个web应用程序,它工作得很好。我发现它是React Vue等的一个很好的替代品(当然它要简单得多,而且不像React那样做…)
关于这个代码审查问题的两点观察:
重构- 添加一个辅助函数来创建一个具有可选文本内容的元素,以及
- 删除
video
对象抽象,因为它复制了三个相同名称下的两个属性,
生成可读但普通类型的Javascript:
function loadResults(array) {
function create (type,text) {
let element = document.createElement(type);
if( text)
element.appendChild( document.createTextNode( text));
return element;
}
array.forEach(videoObject => {
let vid = videoObject.snippet;
let div = create("DIV");
let img = create("IMG");
img.src = vid.thumbnails.default.url;
let h4 = create("H4", vid.title);
let p = create("P", vid.description);
div.appendChild(img);
div.appendChild(h4);
div.appendChild(p);
document.getElementById('results').appendChild(div);
});
}
- 在单击任何位置时隐藏元素,而不检查每次DOM单击
- 是否有任何snippet或jQuery插件可以列出easylist.txt模式匹配的DOM中的所有元素
- 在不使用JQuery的情况下隐藏DOM中的选定元素
- 如何在DOM元素上按类型构建此函数
- DOM元素和angular元素之间的主要区别是什么
- 当带有渲染器的DOM元素不在屏幕顶部时,移动了场景的坐标
- 如何使用JavaScript在没有html dom的情况下隐藏html元素
- 使用jquery创建dom元素会导致ie9出现拒绝访问错误
- 通过AJAX侦听向DOM添加某些元素
- 如何在使用Ractive.extend()时引用DOM元素
- 在d3中向DOM元素添加了图像,但现在它赢得了't过渡
- Selenium无法在浏览器DOM中定位元素
- 如何'剪切'DOM元素并将其显示在其他位置
- 转换<a>使用jQuery将文本字符串转换为dom元素
- 从dom中删除任何元素后,Touchmove事件停止触发
- d3在数据更新时错误地附加了dom元素
- 访问VueJS中的DOM元素
- 在Meteor中如何查找DOM元素(渲染后)
- 找到元素DOM的根节点(阴影或光)的最佳方法是什么?
- 在querySelector:如何获得第一个和最后一个元素?dom中使用的遍历顺序