109

I have the the following code

<b class="xyzxterms" style="cursor: default; ">bryant keil bio</b>

How would I replace the b tag to a h1 tag but keep all other attributes and information?

Felix Kling
  • 705,106
  • 160
  • 1,004
  • 1,072
bammab
  • 2,383
  • 7
  • 23
  • 27

11 Answers11

141

Here's one way you could do it with jQuery:

var attrs = { };

$.each($("b")[0].attributes, function(idx, attr) {
    attrs[attr.nodeName] = attr.nodeValue;
});


$("b").replaceWith(function () {
    return $("<h1 />", attrs).append($(this).contents());
});

Example: http://jsfiddle.net/yapHk/

Update, here's a plugin:

(function($) {
    $.fn.changeElementType = function(newType) {
        var attrs = {};

        $.each(this[0].attributes, function(idx, attr) {
            attrs[attr.nodeName] = attr.nodeValue;
        });

        this.replaceWith(function() {
            return $("<" + newType + "/>", attrs).append($(this).contents());
        });
    };
})(jQuery);

Example: http://jsfiddle.net/mmNNJ/

Andrew Whitaker
  • 119,029
  • 30
  • 276
  • 297
  • +1 Nice. Regarding the content, you can do: `$("

    ", attrs).append($(this).children());`. This would also preserve any data and event handlers attached to child nodes.
    – Felix Kling Dec 21 '11 at 01:54
  • 2
    @FelixKling: Thanks, `children` didn't work but `contents` did. – Andrew Whitaker Dec 21 '11 at 01:56
  • Ah right... it had to be `.contents()`, forgot about the text nodes. – Felix Kling Dec 21 '11 at 01:56
  • i just noticed there are other b tags that get changed is is possible to change based on class? – bammab Dec 21 '11 at 02:04
  • @bammab: sure, you would change all the instances of `"b"` to `"b.class`". One second and I'll wrap it in a plugin. – Andrew Whitaker Dec 21 '11 at 02:05
  • @bammab: There's a plugin that will do it. You can use `$("b.class").changeElementType("h1");` – Andrew Whitaker Dec 21 '11 at 02:09
  • 1
    @Andrew Whitaker WOW!!! you're good! So just to be sure I use b.class or b.xyzxterms (xyzxterms being the name of the class) – bammab Dec 21 '11 at 02:15
  • @AndrewWhitaker: Where's the plugin? – qwertymk Dec 21 '11 at 02:41
  • @qwertymk: The code wrapped with `(function($) { ... })(jQuery)` – Andrew Whitaker Dec 21 '11 at 13:33
  • 5
    @AndrewWhitaker: If I'm not wrong, in your plugin, the attributes of the first matched element will be applied to all matching elements. It's not necessarily what we want. Also an error is raise when there's not matched element in the set. Here's a modified version of your plugin that keep own attributes for each matched elements and do not trigger an error on empty set: https://gist.github.com/2934516 – Etienne Jun 15 '12 at 03:33
  • @Etienne: Yes, I think you're right in both cases. Thanks for the update! – Andrew Whitaker Jun 15 '12 at 12:27
  • 2
    This works like a charm! Except that when the selector fails to find any matching element, it throws an error message to console because this[0] is undefined accessing attributes breaks. Adding a condition fixes it: if (this.length != 0) {... – ciuncan Sep 10 '13 at 19:58
  • 1
    @ciuncan: Thanks for the feedback! It should really be wrapped in an `.each` block like an answer below shows too. – Andrew Whitaker Sep 10 '13 at 20:06
  • 1
    Can someone explain to me what's going on with the $.each() there? – MetalPhoenix May 30 '17 at 19:36
  • 1
    @MetalPhoenix Late response, but in case it helps anyone: That's the jQuery $.each function, documented at http://api.jquery.com/jquery.each/ and is *not* the same as jQuery $(selector).each() function. It's taking the attribute array and iterating over it. Worth reiterating: For the general case, people should use Eitienne's Gist version, since that reads the attributes of each separate element, instead of relying on the attribute array of the first matched element. – Jacob C. Oct 05 '17 at 19:10
  • "attribute array and iterating over it" Gotcha. I'd never seen it written that way where the collection was inside the .each, always $("something").each(function(){}); Thanks – MetalPhoenix Oct 05 '17 at 19:18
  • Goodness, why is this not built into jQuery yet? Seems a very useful DOM manipulation when you're, say, working with third party HTML. Plugin is simple enough, though, tnx. – Phil Tune Jan 03 '18 at 15:33
17

Not sure about jQuery. With plain JavaScript you could do:

var new_element = document.createElement('h1'),
    old_attributes = element.attributes,
    new_attributes = new_element.attributes;

// copy attributes
for(var i = 0, len = old_attributes.length; i < len; i++) {
    new_attributes.setNamedItem(old_attributes.item(i).cloneNode());
}

// copy child nodes
do {
    new_element.appendChild(element.firstChild);
} 
while(element.firstChild);

// replace element
element.parentNode.replaceChild(new_element, element);

DEMO

Not sure how cross-browser compatible this is though.

A variation could be:

for(var i = 0, len = old_attributes.length; i < len; i++) {
    new_element.setAttribute(old_attributes[i].name, old_attributes[i].value);
}

For more information see Node.attributes [MDN].

Felix Kling
  • 705,106
  • 160
  • 1,004
  • 1,072
  • The performance of your code is better than the "pure jQuery" (ex. Andrew's code), but have a little problem with inner tags, see italic at [this example with your code](http://jsfiddle.net/5NHvw/17/) and the [reference-example](http://jsfiddle.net/mmNNJ/194/). – Peter Krauss Mar 31 '14 at 22:26
  • If you correct, [an "ideal jquery plugin"](http://jsfiddle.net/mmNNJ/195/) can be defined, calling your function by the jquery-plugin-template. – Peter Krauss Mar 31 '14 at 22:32
  • Fixed. The problem was that after the copying over the first child, it doesn't have a next sibling anymore, so `while(child = child.nextSibling)` failed. Thanks! – Felix Kling Mar 31 '14 at 22:35
10

@jakov and @Andrew Whitaker

Here is a further improvement so it can handle multiple elements at once.

$.fn.changeElementType = function(newType) {
    var newElements = [];

    $(this).each(function() {
        var attrs = {};

        $.each(this.attributes, function(idx, attr) {
            attrs[attr.nodeName] = attr.nodeValue;
        });

        var newElement = $("<" + newType + "/>", attrs).append($(this).contents());

        $(this).replaceWith(newElement);

        newElements.push(newElement);
    });

    return $(newElements);
};
Jazzbo
  • 101
  • 1
  • 2
4

@Jazzbo's answer returned a jQuery object containing an array of jQuery objects, which wasn't chainable. I've changed it so that it returns an object more similar to what $.each would have returned:

    $.fn.changeElementType = function (newType) {
        var newElements,
            attrs,
            newElement;

        this.each(function () {
            attrs = {};

            $.each(this.attributes, function () {
                attrs[this.nodeName] = this.nodeValue;
            });

            newElement = $("<" + newType + "/>", attrs).append($(this).contents());

            $(this).replaceWith(newElement);

            if (!newElements) {
                newElements = newElement;
            } else {
                $.merge(newElements, newElement);
            }
        });

        return $(newElements);
    };

(Also did some code cleanup so it passes jslint.)

fiskhandlarn
  • 186
  • 1
  • 9
  • This seems like the nicest option. The only thing I don't get is why you moved the var declaration for attrs out of the this.each(). It works fine with it left there: http://jsfiddle.net/9c0k82sr/1/ – Jacob C. Oct 10 '17 at 22:53
  • I grouped the vars because of jslint: "(Also did some code cleanup so it passes jslint.)". The idea behind that is to make the code faster, i think (not having to redeclare vars within each `each` loop). – fiskhandlarn Oct 12 '17 at 14:19
2

@Andrew Whitaker: I propose this change:

$.fn.changeElementType = function(newType) {
    var attrs = {};

    $.each(this[0].attributes, function(idx, attr) {
        attrs[attr.nodeName] = attr.nodeValue;
    });

    var newelement = $("<" + newType + "/>", attrs).append($(this).contents());
    this.replaceWith(newelement);
    return newelement;
};

Then you can do things like: $('<div>blah</div>').changeElementType('pre').addClass('myclass');

jakov
  • 551
  • 4
  • 3
2

I like the idea of @AndrewWhitaker and others, to use a jQuery plugin -- to add the changeElementType() method. But a plugin is like a blackbox, no mater about the code, if it is litle and works fine... So, performance is required, and is most important than code.

"Pure javascript" have better performance than jQuery: I think that @FelixKling's code have better performance than @AndrewWhitaker's and others.


Here a "pure Javavascript" (and "pure DOM") code, encapsulated into a jQuery plugin:

 (function($) {  // @FelixKling's code
    $.fn.changeElementType = function(newType) {
      for (var k=0;k<this.length; k++) {
       var e = this[k];
       var new_element = document.createElement(newType),
        old_attributes = e.attributes,
        new_attributes = new_element.attributes,
        child = e.firstChild;
       for(var i = 0, len = old_attributes.length; i < len; i++) {
        new_attributes.setNamedItem(old_attributes.item(i).cloneNode());
       }
       do {
        new_element.appendChild(e.firstChild);
       }
       while(e.firstChild);
       e.parentNode.replaceChild(new_element, e);
      }
      return this; // for chain... $(this)?  not working with multiple 
    }
 })(jQuery);
Peter Krauss
  • 11,340
  • 17
  • 129
  • 247
2

Here is a method I use to replace html tags in jquery:

// Iterate over each element and replace the tag while maintaining attributes
$('b.xyzxterms').each(function() {

  // Create a new element and assign it attributes from the current element
  var NewElement = $("<h1 />");
  $.each(this.attributes, function(i, attrib){
    $(NewElement).attr(attrib.name, attrib.value);
  });

  // Replace the current element with the new one and carry over the contents
  $(this).replaceWith(function () {
    return $(NewElement).append($(this).contents());
  });

});
Seth McCauley
  • 864
  • 11
  • 24
2

With jQuery without iterating over attributes:

The replaceElem method below accepts old Tag, new Tag and context and executes the replacement successfully:


replaceElem('h2', 'h1', '#test');

function replaceElem(oldElem, newElem, ctx) {
  oldElems = $(oldElem, ctx);
  //
  $.each(oldElems, function(idx, el) {
    var outerHTML, newOuterHTML, regexOpeningTag, regexClosingTag, tagName;
    // create RegExp dynamically for opening and closing tags
    tagName = $(el).get(0).tagName;
    regexOpeningTag = new RegExp('^<' + tagName, 'i'); 
    regexClosingTag = new RegExp(tagName + '>$', 'i');
    // fetch the outer elem with vanilla JS,
    outerHTML = el.outerHTML;
    // start replacing opening tag
    newOuterHTML = outerHTML.replace(regexOpeningTag, '<' + newElem);
    // continue replacing closing tag
    newOuterHTML = newOuterHTML.replace(regexClosingTag, newElem + '>');
    // replace the old elem with the new elem-string
    $(el).replaceWith(newOuterHTML);
  });

}
h1 {
  color: white;
  background-color: blue;
  position: relative;
}

h1:before {
  content: 'this is h1';
  position: absolute;
  top: 0;
  left: 50%;
  font-size: 5px;
  background-color: black;
  color: yellow;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>


<div id="test">
  <h2>Foo</h2>
  <h2>Bar</h2>
</div>

Good Luck...

Aakash
  • 14,077
  • 4
  • 77
  • 63
  • 1
    I like your answer! Why? Because ALL of the other answers will fail in attempting to do something simple like convert an anchor to a label. That said, please consider the following fixes/revisions to your answer: A). Your code won't work with selectors. B) Your code needs to perform case insensitive regex. That said, here are my suggested fixes: regexOpeningTag = new RegExp('^$', 'i'); – zax Jul 09 '18 at 01:32
  • Replacing plain HTML like this will also make you lose any event listeners attached to the objects. – José Yánez Jul 31 '19 at 01:35
2

Only way I can think of is to copy everything over manually: example jsfiddle

HTML

<b class="xyzxterms" style="cursor: default; ">bryant keil bio</b>

Jquery/Javascript

$(document).ready(function() {
    var me = $("b");
    var newMe = $("<h1>");
    for(var i=0; i<me[0].attributes.length; i++) {
        var myAttr = me[0].attributes[i].nodeName;
        var myAttrVal = me[0].attributes[i].nodeValue;
        newMe.attr(myAttr, myAttrVal);
    }
    newMe.html(me.html());
    me.replaceWith(newMe);
});
kasdega
  • 16,576
  • 12
  • 40
  • 83
1

Javascript solution

Copy the attributes of old element to the new element

const $oldElem = document.querySelector('.old')
const $newElem = document.createElement('div')

Array.from($oldElem.attributes).map(a => {
  $newElem.setAttribute(a.name, a.value)
})

Replace the old element with the new element

$oldElem.parentNode.replaceChild($newElem, $oldElem)
svnm
  • 17,405
  • 18
  • 82
  • 100
1

Here is my version. It's basically @fiskhandlarn's version, but instead of constructing a new jQuery object, it simply overwrites the old elements with the newly created ones, so no merging is necessary.
Demo: http://jsfiddle.net/0qa7wL1b/

$.fn.changeElementType = function( newType ){
  var $this = this;

  this.each( function( index ){

    var atts = {};
    $.each( this.attributes, function(){
      atts[ this.name ] = this.value;
    });

    var $old = $(this);
    var $new = $('<'+ newType +'/>', atts ).append( $old.contents() );
    $old.replaceWith( $new );

    $this[ index ] = $new[0];
  });

  return this;
};
biziclop
  • 13,654
  • 3
  • 45
  • 62