Catch strings not wrapped in span

I have an html root element looks like this:

<div>
    <span>first</span>
    second
    <span>third</span>
    fourth
<div>

Now i need to wrap third and fourth with span like others, So the result will be:

<div>
    <span>first</span>
    <span>second</span>
    <span>third</span>  
    <span>fourth</span>
</div>

Loop through the div children, and if the child is a text element, and after trimming it it still has length, put a span before that child element and wrap it in a span.

Do you want to give this a go yourself?

I put together a fiddle that achieves what you want, at https://jsfiddle.net/pmw57/Ltg7j5vr/

What follows is a breakdown of my above explanation, to describe how it works:

Make it easy to see progress

I started with your HTML code:

<div>
    <span>first</span>
    second
    <span>third</span>
    fourth
<div>

and colored the text red, with span text being colored green.

div {
    color: red;
}
div span {
    color: green;
}

That way I had a nice visual way to give me immediate feedback on my progress.

Get the elements

I’ve used a simple div selector in my example, but you can modify the selector to target the exact content that you need.

    const div = document.querySelector("div");
    div.childNodes.forEach(wrapTextContent);

The childNodes method gives us all child nodes that div section. If I wanted to ignore non-element things like text nodes or comments I would use children instead. But, we are explicitly wanting to include those so childNodes it is.

I also find it really useful to use separate functions such as wrapTextContent, as that lets us put off the details of how they are done until later, and the function name tells us exactly what we expect to achieve.

Ignore things that aren’t text

It can be tempting to just check if the element is a text element, but then you end up having deeply nested code. Instead of that, check if it’s not a text element and do an early return. That way you are using what’s called a guard clause, to protect against unwanted things.

We just need to check if the nodeType is a text node. There’s a range of values for the different types of nodes. Historically I would just check if it’s value equals 3, but now we have Node.TEXT_NODE which means the same thing, and is easier for people to understand when looking at the code.

    function wrapTextContent(node) {
        if (node.nodeType !== Node.TEXT_NODE) {
            return;
        }
        ...
    }

Ignore empty text areas

When the node is confirmed as being a text node, we can then get the text content of the node and trim it. If the trimmed content doesn’t have any length to it, we don’t want to deal with that and can make an early return too.

    function wrapTextContent(node) {
        ...
        if (node.textContent.trim() === 0) {
            return;
        }
        ...
    }

Add a span element

After confirming that it’s a text node with content, we can add a span element to the page.
It makes sense for the task of wrapping content with a span to be contained in a separate function, so I’ve used a separate wrapNode function for that.

    function wrapTextContent(node) {
        ...
        const span = document.createElement("span");
        wrapNode(node, span);
    }

The part of the wrapNode function that puts the span before the node is as follows:

    function wrapNode(node, wrapper) {
            node.parentNode.insertBefore(wrapper, node);
            ...
    }

Move the text content into the span

This last part is really easy, for the appendChild method actually moves instead, when the elements is already in the DOM.

    function wrapNode(node, wrapper) {
            ...
            wrapper.appendChild(node);
    }

Protect the local namespace

Another thing that I did with the code is to put it in an IIFE (immediately invoked function expression). That protects the local namespace from my code, so that function names and variables don’t end up leaking out to affect other code that might run.

(function iife() {
    ...
}());

It doesn’t have to be called iife either. Instead I find that it’s beneficial to give it a meaningful name for what the code is supposed to achieve.

(function wrapUncontainedTextWithSpan() {
    ...
}());

Make it easy to configure

The div selector is the main thing to configure with this code. To achieve that, instead of using a separate IIFE for the code, we can remove the immediately invoked part and use it as a separate function instead.

The parameter that the function uses I’ve named parentSelector. If it was just called selector that might become confusing, so I’ve deliberately named it as parentSelector to help inform us that a parent selector is required.

// (function wrapUncontainedTextWithSpan() {
function wrapUncontainedTextWithSpan(parentSelector) {
    ...
// }());
}
wrapUncontainedTextWithSpan("div");

I can now update the last part of the code, so that instead of using its own div, it uses the parentSelector instead.

    // const div = document.querySelector("div");
    const node = document.querySelector(parentSelector);
    // div.childNodes.forEach(wrapTextContent);
    node.childNodes.forEach(wrapTextContent);

And we can even make it capable of dealing with multiple matching parent selectors:

    // const node = document.querySelector(parentSelector);
    const nodes = document.querySelectorAll(parentSelector);
    nodes.forEach(function wrapEachTextContent(node) {
        node.childNodes.forEach(wrapTextContent);
    });

Summary

And that’s it. It’s just a matter of putting all of these simple things together.

The resulting code is:

function wrapUncontainedTextWithSpan(parentSelector) {
    function wrapNode(node, wrapper) {
            node.parentNode.insertBefore(wrapper, node);
            wrapper.appendChild(node);
    }
    function wrapTextContent(node) {
        if (node.nodeType !== Node.TEXT_NODE) {
            return;
        }
        if (node.textContent.trim() === 0) {
            return;
        }
        const span = document.createElement("span");
        wrapNode(node, span);
    }

    const nodes = document.querySelectorAll(parentSelector);
    nodes.forEach(function wrapEachTextContent(node) {
        node.childNodes.forEach(wrapTextContent);
    });
}

We run that code using a suitable parent selector:

wrapUncontainedTextWithSpan("div");

The full code for doing this is found at https://jsfiddle.net/pmw57/Ltg7j5vr/

5 Likes

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.