Let's say I have a few UI Components in React JS, which render some HTML on screen, like below:
<div >
<h1 >Heading Text</h1>
<div >Section Text</div>
</div>
On screen, this is displayed as proper HTML. What I want is the ability for the user to be able to select (by clicking) this block, which should display the element tree along with the classes applied to each element on one side of the page. Then, my goal is to allow users to change the classes applied to see how the UI changes with the change in classes.
I want to be able to do this react/javascript without using any library such as grapesjs.
Can you please guide me on how I can go about achieving this?
Update: One thing I did is that I added a function onClick={(e) => handleClick(e)} to the block div.
Now within handleClick, I could get the element's classed:
const handleClick = (e) => {
console.log('E', e.target.classList);
e.target.classList.add(styles.borderline);
}
Now the problem is, if there are multiple blocks on the page, how will I know which block is selected? Eventually, I want to save/replace the data related to the block in my database. So, I need to be able to know which block was updated, and save its data.
CodePudding user response:
There are three parts to this solution:
- Stateful class values
- Displaying the tree
- Binding the stateful class values to inputs
(1)
Using functional components with React hooks, you can initialize a variable for each default class name with the useState hook as follows:
const [containerClass, setContainerClass] = useState("content-block margin-10");
const [headingClass, setHeadingClass] = useState("block-head");
const [sectionClass, setSectionClass] = useState("section-text bg-gray text-md");
This sets their default values to the values in the question description and ensures that when these values are updated, any HTML that depends on these values will re-render. Thus, in order to bind these to the elements in question, we can use the following classNames as opposed to their hard-coded counterparts:
<div className={containerClass}>
<h1 className={headingClass}>Heading Text</h1>
<div className={sectionClass}>Section Text</div>
</div>
(2) Displaying the tree requires three things of us: a boolean variable that indicates whether or not the tree is displayed, a click event (as noted in the question) that toggles this variable, and a JSX section that renders when the variable is true.
The variable itself can be a simple useState declaration like the ones above:
const [treeIsDisplayed, setTreeIsDisplayed] = useState(false);
It can be toggled by using the following function as the container div's onClick value:
const toggleTree = () => setTreeIsDisplayed((prev) => !prev);
And we can render the tree conditionally like so:
{treeIsDisplayed && (
<div>
<p>
{"<div ></div>"}
</p>
<p>
{"<h1 ></h1>"}
</p>
<p>
{"<div ></div>"}
</p>
<p>{"</div>"}</p>
</div>
)}
This renders as the following:
Note: The tree will display under the HTML by default, so some CSS will be needed to display it to the side.
(3)
We can now create functions to be utilized by the above inputs to set the class names dynamically:
const changeContainerClass = (e) => setContainerClass(e.target.value);
const changeHeadingClass = (e) => setHeadingClass(e.target.value);
const changeSectionClass = (e) => setSectionClass(e.target.value);
And we set the onChange value of each input to its corresponding change function.
All together, the working solution constructed here looks like this:
const App = () => {
const [containerClass, setContainerClass] = useState("content-block margin-10");
const [headingClass, setHeadingClass] = useState("block-head");
const [sectionClass, setSectionClass] = useState("section-text bg-gray text-md");
const [treeIsDisplayed, setTreeIsDisplayed] = useState(false);
const toggleTree = () => setTreeIsDisplayed((prev) => !prev);
const changeContainerClass = (e) => setContainerClass(e.target.value);
const changeHeadingClass = (e) => setHeadingClass(e.target.value);
const changeSectionClass = (e) => setSectionClass(e.target.value);
return (
<>
<div className={containerClass} onClick={toggleTree}>
<h1 className={headingClass}>Heading Text</h1>
<div className={sectionClass}>Section Text</div>
</div>
{treeIsDisplayed && (
<div>
<p>
{"<div ></div>"}
</p>
<p>
{"<h1 ></h1>"}
</p>
<p>
{"<div ></div>"}
</p>
<p>{"</div>"}</p>
</div>
)}
</>
)
}
CodePudding user response:
var elementTree;
function convertToHTML(tree) {
let elementTree = `<table>`;
elementTree = `
<thead>
<tr>
<th>Element Tree</th>
<th>Classes</th>
</tr>
</thead>`;
tree.map((element, elementIndex) => {
elementTree = `
<tr>
<td>${element.element}</td>
<td>
<textarea onchange=changeClass(event) data-target='${elementIndex}'>${element.classList.join(" ")}</textarea>
</td>
</tr>`;
});
elementTree = `</table>`
return elementTree;
}
function changeClass(event) {
let textArea = event.target;
let oldClass = textArea.defaultValue.trim().split(" ");
let newClass = textArea.value.trim().split(" ");
let index = textArea.dataset.target;
if (oldClass[0]) {
elementTree[index].target.classList.remove(...oldClass)
}
elementTree[index].target.classList.add(...newClass)
console.log(oldClass, newClass);
}
function getElementTree(event) {
var target = event.target,
elementTree = [];
while (target) {
let classList = target.classList;
elementTree.unshift({
element: target.tagName,
classList: target.classList.value.split(" "),
target: target
});
target = target.parentElement;
}
console.log(elementTree);
return elementTree;
}
function showElementTree(tree) {
document.getElementsByClassName("element-tree")[0].innerHTML = tree;
}
document.getElementsByClassName('element')[0].addEventListener('click', function(event) {
elementTree = getElementTree(event);
let elementTreeHTML = convertToHTML(elementTree);
showElementTree(elementTreeHTML);
}, false);
.main {
display: flex;
}
.main>div {
border: 1px solid black;
padding: 10px;
}
table,
th,
tr,
td {
padding: 5px;
border: 1px solid black;
border-collapse: collapse;
}
<div >
<div >
<div >
<h1 >Heading Text</h1>
<div >
<p>element tree</p>
</div>
</div>
</div>
<div >
</div>
</div>
A vanilla JS solution would be to use eventHandlers and hold the target variable. Whenever the class changes the associated classList property of the target object is changed

