How to perform this dynamic Find and replace in Javascript over user generated data?

71 Views Asked by At

I have an app that allows user to generate text with HTML code in the following format:

<h2>User generated Dynamic Data 1</h2>
    <h3>User generated text 1.1</h3>
    <h3>User generated text 1.2</h3>


<h2>User generated Dynamic Data 2</h2>
    <h3>User generated text 2.1</h3>
    <h3>User generated text 2.2</h3>
    <h3>User generated text 2.3</h3>
    
<h2>User generated Dynamic Data 3</h2>
    <h3>User generated text 3.1</h3>
    <h3>User generated text 3.2</h3>

This is how it looks like in a browser:

This is how it looks like in a browser

Is there any way to replace what user generated with the one below, using javascript?

<h2>User generated Dynamic Data 1 <button class="something" onclick="bubble_fn_add_headers({output1: 'User generated Dynamic Data 1', output2: 'User generated text 1.1\nUser generated text 1.2'});">Save</button></h2>
    <h3>User generated text 1.1</h3>
    <h3>User generated text 1.2</h3>


<h2>User generated Dynamic Data 2 <button class="something" onclick="bubble_fn_add_headers({output1: 'User generated Dynamic Data 2', output2: 'User generated text 2.1\nUser generated text 2.2\nUser generated text 2.3'});">Save</button></h2>
    <h3>User generated text 2.1</h3>
    <h3>User generated text 2.2</h3>
    <h3>User generated text 2.3</h3>
    
    
<h2>User generated Dynamic Data 3 <button class="something" onclick="bubble_fn_add_headers({output1: 'User generated Dynamic Data 3', output2: 'User generated text 3.1\nUser generated text 2.2'});">Save</button></h2>
    <h3>User generated text 3.1</h3>
    <h3>User generated text 3.2</h3>    

This is how the above would look like in a browser:

This is how the above would look like in a browser

The situation is very trickey because:

  • All the texts surrounded by <h2></h2> and <h3></h3> tags are user generated.
  • Users can generate any number of <h2> Texts followed by any or even zero number of <h3> texts.

Can you guys suggest any work around this using javascript?

Thanks

I would have tried

s.replace('<h2>', '<h2>User generated Dynamic Data 1 <button class="something" onclick="bubble_fn_add_headers({output1: 'User generated Dynamic Data 1', output2: 'User generated text 1.1\nUser generated text 1.2'});">Save</button></h2>')

But it just isn't possible because the texts are dynamically generated and are unique each time.

5

There are 5 best solutions below

10
mplungjan On BEST ANSWER

Don't use regex or replace to change HTML.

Just use DOM access

Here is the minimum safe way to create the object and embed it in a button

const nextUntil = (element, selectors, filter = "*") => {
  const siblings = [element];
  let next = element.nextElementSibling;

  while (next && !next.matches(selectors)) {
    if (next.matches(filter))
      siblings.push(next);
    next = next.nextElementSibling;
  }

  return siblings;
};

document.querySelectorAll('h2').forEach(header => {
  const subs = nextUntil(header,'h2','h3')
  //console.log(subs)
  const object = {output1: subs[0].textContent,output2: subs.slice(1).map(ele => ele.textContent).join('\n')}
  header.innerHTML = `${header.textContent} 
    <button class="something" 
    onclick='bubble_fn_add_headers(${JSON.stringify(object)})'>Save</button>`;
}) 
  
const bubble_fn_add_headers = obj => console.log(obj);

/* I recommend the following instead of inline onclick

document.body.addEventListener('click', (e) => {
  const tgt = e.target.closest('button.something');
  if (!tgt) return; 
  bubble_fn_add_headers(tgt.dataset.content)
})

using this code for the data-attribute

    data-content='${
    JSON.stringify({ 
      "output1": subs[0].textContent, 
      "output2": subs.slice(1).map(ele => ele.textContent).join('\n')
    })}'
*/
<h2>User generated Dynamic Data 1</h2>
<h3>User generated text 1.1</h3>
<h3>User generated text 1.2</h3>


<h2>User generated Dynamic Data 2</h2>
<h3>User generated text 2.1</h3>
<h3>User generated text 2.2</h3>
<h3>User generated text 2.3</h3>

<h2>User generated Dynamic Data 3</h2>
<h3>User generated text 3.1</h3>
<h3>User generated text 3.2</h3>

Here is what you asked for. It is VERY BRITTLE and will blow up the first time a user enters a quote or a double quote

const nextUntil = (element, selectors, filter = "*") => {
  const siblings = [element];
  let next = element.nextElementSibling;

  while (next && !next.matches(selectors)) {
    if (next.matches(filter))
      siblings.push(next);
    next = next.nextElementSibling;
  }

  return siblings;
};

document.querySelectorAll('h2').forEach(header => {
  const subs = nextUntil(header,'h2','h3')
  //console.log(subs)
  const string = `{output1: '${subs[0].textContent}, output2: '${subs.slice(1).map(ele => ele.textContent).join('\\n')}`
  header.innerHTML = `${header.textContent} 
    <button class="something" 
    onclick="bubble_fn_add_headers('${string}')">Save</button>`;
}) 
  
const bubble_fn_add_headers = obj => console.log(obj);

/* I recommend the following instead of inline onclick

document.body.addEventListener('click', (e) => {
  const tgt = e.target.closest('button.something');
  if (!tgt) return; 
  bubble_fn_add_headers(tgt.dataset.content)
})

using this code for the data-attribute

    data-content='${
    JSON.stringify({ 
      "output1": subs[0].textContent, 
      "output2": subs.slice(1).map(ele => ele.textContent).join('\n')
    })}'
*/
<h2>User generated Dynamic Data 1</h2>
<h3>User generated text 1.1</h3>
<h3>User generated text 1.2</h3>


<h2>User generated Dynamic Data 2</h2>
<h3>User generated text 2.1</h3>
<h3>User generated text 2.2</h3>
<h3>User generated text 2.3</h3>

<h2>User generated Dynamic Data 3</h2>
<h3>User generated text 3.1</h3>
<h3>User generated text 3.2</h3>

3
nkt217 On

You can use regex for this. See the fiddle for more details.

Code::

let content = `
<h2>User generated Dynamic Data 1</h2>
    <h3>User generated text 1.1</h3>
    <h3>User generated text 1.2</h3>


<h2>User generated Dynamic Data 2</h2>
    <h3>User generated text 2.1</h3>
    <h3>User generated text 2.2</h3>
    <h3>User generated text 2.3</h3>
    
<h2>User generated Dynamic Data 3</h2>
    <h3>User generated text 3.1</h3>
    <h3>User generated text 3.2</h3>
`;

// -- find text between a <h2> tag
const stext = content.match(/(?<=<h2>).*(<\/h2>)|(?<=<h3>).*(?=<\/h3>)/g);
console.log('stext:: ', stext);

let output1 = '', output2 = '', count = 0;
stext.forEach(text => {
    if(text.indexOf('</h2>') > -1) {
        // -- every time a primary text is found update the user generated content. This will run 1 count behind the main counter
    if(count > 0) {
        content = content.replace(output1, `${output1}<button class="something" onclick="bubble_fn_add_headers({output1: '${output1}', output2: ${output2}});">Save</button>`)
    }
    // -- this is for primary text
    output1 = text.replace('</h2>', '');
    output2 = ''; // -- resetting this
    count++;
  }
  else {
    // -- this is for sub-text
    output2 += `${text}\n`; 
  }
});
content = content.replace(output1, `${output1}<button class="something" onclick="bubble_fn_add_headers({output1: '${output1}', output2: ${output2}});">Save</button>`)

document.querySelector('body').innerHTML = content;
1
Deepak D On

Consider generating the HTML elements dynamically from the user input, which is easier and more scalable:

  1. Get user input value using querySelector()
  2. Store the input value in an array
  3. Generate a list of "li" or "div", using createElement() and appendChild()

Working example is here.

As for generating more complex/nested elements, better approach would be to store data as JSON object, and render by referencing the keys..

2
Graeme Stuart On

Loop over the headings with querySelectorAll

One solution would be to iterate over your content, extract the necessary text and generate the buttons dynamically, adding custom event handlers to each button.

If I understood the problem, this should work.

const myHeaders = document.querySelectorAll('h2');

let el;
for (const h2 of myHeaders) {

    const output1 = h2.textContent;
    let output2 = [];
    el = h2.nextElementSibling;

    while (el && el.matches('h3')) {
        console.log(el.textContent);
        output2.push(el.textContent);
        el = el.nextElementSibling;
    }

    const btn = document.createElement('button');
    btn.class = "something";
    btn.textContent = "Save";
    btn.addEventListener('click', ev => {
        bubble_fn_add_headers({ output1, output2: output2.join('\n') });        
    });    
    h2.append(btn);
}

function bubble_fn_add_headers(data) {
    console.log(data);
}

However, I would question a number of things about the system that generated this output in the first place. One thing is that your HTML seems to have very little structure. Perhaps each <h2> element could be enclosed in a <section> for example. This would make the task a lot easier because you could loop over the sections and extract the <h2> and <h3> elements using simple queries.

0
Peter Seliger On

A single function does the trick.

It has to be passed as callback to the forEach method of a NodeList which got obtained by querying all H2 elements from the before parsed html markup.

Said function creates and inserts a button element with inline-handler code which got collected/accumulated from each header-related text-content.

With the mutation of each H2 element node, the before parsed html document gets mutated as well. Thus, after having processed each H2 element one can do both, continue working with the parsed and mutated DOM and/or reading its innerHTML which should be equal to the final markup the OP is looking for.

const htmlDoc = (new DOMParser)
  .parseFromString(sourceMarkup, 'text/html');

htmlDoc
  .body
  .querySelectorAll('h2')
  .forEach(insertButtonWithHeaderContentHandler);

const targetMarkup = htmlDoc.body.innerHTML;

document.body.innerHTML = targetMarkup;

console.log({ targetMarkup });
:root { font-size: .8em; }
body { margin: 0; }
h2, h3 { width: 30%; }
.as-console-wrapper { left: auto!important; min-height: 100%; width: 70%; }
<script>
function insertButtonWithHeaderContentHandler(
  node, idx, nodeList
) {
  let nextNode;
  let subContent = '';

  while (
    (node = node.nextSibling) &&
    (node !== nodeList[idx + 1])
  ) {
    subContent += node.textContent;
  }
  node = nodeList[idx];

  subContent = subContent
    .trim()
    .replace(/\s*\n+\s*/g, '\\n')
    .replace(/^\n+|\n+$/g, '')
    .replace(/\s+/g, ' ')

  const inlineHandlerCode =
    `bubble_fn_add_headers({output1: "${ node.textContent }", output2: "${ subContent }" });`

  const elmButton = document.createElement('button');

  elmButton.textContent = 'Save';
  elmButton.classList.add('something');
  elmButton.setAttribute('onclick', inlineHandlerCode);

  node.appendChild(elmButton);
}

const sourceMarkup = `
<h2>User generated Dynamic Data 1</h2>
    <h3>User generated text 1.1</h3>
    <h3>User generated text 1.2</h3>


<h2>User generated Dynamic Data 2</h2>
    <h3>User generated text 2.1</h3>
    <h3>User generated text 2.2</h3>
    <h3>User generated text 2.3</h3>
    
    
<h2>User generated Dynamic Data 3</h2>
    <h3>User generated text 3.1</h3>
    <h3>User generated text 3.2</h3>
`;

function bubble_fn_add_headers({ output1, output2 }) {
  console.log({ output1, output2 });
}
</script>