Get unique selector of element in Jquery

JavascriptJqueryHtmlCss Selectors

Javascript Problem Overview


I want to create something like a recorder whichs tracks all actions of a user. For that, i need to identify elements the user interacts with, so that i can refer to these elements in a later session.

Spoken in pseudo-code, i want to be able to do something like the following

Sample HTML (could be of any complexity):

<html>
<body>
  <div class="example">
    <p>foo</p>
    <span><a href="bar">bar</a></span>
  </div>
</body>
</html>
  

User clicks on something, like the link. Now i need to identify the clicked element and save its location in the DOM tree for later usage:

(any element).onclick(function() {
  uniqueSelector = $(this).getUniqueSelector();
})

Now, uniqueSelector should be something like (i don't mind if it is xpath or css selector style):

html > body > div.example > span > a

This would provide the possibility to save that selector string and use it at a later time, to replay the actions the user made.

How is that possible?

Update

Got my answer: https://stackoverflow.com/questions/2068272/getting-a-jquery-selector-for-an-element

Javascript Solutions


Solution 1 - Javascript

I'll answer this myself, because i found a solution which i had to modify. The following script is working and is based on a script of Blixt:

jQuery.fn.extend({
	getPath: function () {
		var path, node = this;
		while (node.length) {
			var realNode = node[0], name = realNode.name;
			if (!name) break;
			name = name.toLowerCase();

			var parent = node.parent();

			var sameTagSiblings = parent.children(name);
			if (sameTagSiblings.length > 1) { 
				var allSiblings = parent.children();
				var index = allSiblings.index(realNode) + 1;
				if (index > 1) {
					name += ':nth-child(' + index + ')';
				}
			}

			path = name + (path ? '>' + path : '');
			node = parent;
		}

		return path;
	}
});

Solution 2 - Javascript

Same solution like that one from @Alp but compatible with multiple jQuery elements.

jQuery('.some-selector') can result in one or many DOM elements. @Alp's solution works unfortunately only with the first one. My solution concatenates all them with ,.

If you want just handle the first element do it like this:

jQuery('.some-selector').first().getPath();

// or
jQuery('.some-selector:first').getPath();

Improved version

jQuery.fn.extend({
	getPath: function() {
		var pathes = [];

		this.each(function(index, element) {
			var path, $node = jQuery(element);

			while ($node.length) {
				var realNode = $node.get(0), name = realNode.localName;
				if (!name) { break; }

				name = name.toLowerCase();
				var parent = $node.parent();
				var sameTagSiblings = parent.children(name);

				if (sameTagSiblings.length > 1)
				{
					var allSiblings = parent.children();
					var index = allSiblings.index(realNode) + 1;
					if (index > 0) {
						name += ':nth-child(' + index + ')';
					}
				}

				path = name + (path ? ' > ' + path : '');
				$node = parent;
			}

			pathes.push(path);
		});

		return pathes.join(',');
	}
});

Solution 3 - Javascript

I think a better solution would be to generate a random id and then access an element based on that id:

Assigning unique id:

// or some other id-generating algorithm
$(this).attr('id', new Date().getTime()); 

Selecting based on the unique id:

// getting unique id
var uniqueId = $(this).getUniqueId();

// or you could just get the id:
var uniqueId = $(this).attr('id');

// selecting by id:
var element = $('#' + uniqueId);

// if you decide to use another attribute other than id:
var element = $('[data-unique-id="' + uniqueId + '"]');

Solution 4 - Javascript

(any element).onclick(function() {
  uniqueSelector = $(this).getUniqueSelector();
})

this IS the unique selector and path to that clicked element. Why not use that? You can utilise jquery's $.data() method to set the jquery selector. Alternatively just push the elements you need to use in the future:

var elements = [];
(any element).onclick(function() {
  elements.push(this);
})

If you really need the xpath, you can calculate it using the following code:

  function getXPath(node, path) {
    path = path || [];
    if(node.parentNode) {
      path = getXPath(node.parentNode, path);
    }

    if(node.previousSibling) {
      var count = 1;
      var sibling = node.previousSibling
      do {
        if(sibling.nodeType == 1 && sibling.nodeName == node.nodeName) {count++;}
        sibling = sibling.previousSibling;
      } while(sibling);
      if(count == 1) {count = null;}
    } else if(node.nextSibling) {
      var sibling = node.nextSibling;
      do {
        if(sibling.nodeType == 1 && sibling.nodeName == node.nodeName) {
          var count = 1;
          sibling = null;
        } else {
          var count = null;
          sibling = sibling.previousSibling;
        }
      } while(sibling);
    }

    if(node.nodeType == 1) {
      path.push(node.nodeName.toLowerCase() + (node.id ? "[@id='"+node.id+"']" : count > 0 ? "["+count+"]" : ''));
    }
    return path;
  };

Reference: http://snippets.dzone.com/posts/show/4349

Solution 5 - Javascript

Pure JavaScript Solution

Note: This uses Array.from and Array.prototype.filter, both of which need to be polyfilled in IE11.

function getUniqueSelector(node) {
  let selector = "";
  while (node.parentElement) {
    const siblings = Array.from(node.parentElement.children).filter(
      e => e.tagName === node.tagName
    );
    selector =
      (siblings.indexOf(node)
        ? `${node.tagName}:nth-of-type(${siblings.indexOf(node) + 1})`
        : `${node.tagName}`) + `${selector ? " > " : ""}${selector}`;
    node = node.parentElement;
  }
  return `html > ${selector.toLowerCase()}`;
}

Usage

getUniqueSelector(document.getElementsByClassName('SectionFour')[0]);

getUniqueSelector(document.getElementById('content'));

Solution 6 - Javascript

While the question was for jQuery, in ES6, it is pretty easy to get something similar to @Alp's for Vanilla JavaScript (I've also added a couple lines, tracking a nameCount, to minimize use of nth-child):

function getSelectorForElement (elem) {
    let path;
    while (elem) {
        let subSelector = elem.localName;
        if (!subSelector) {
            break;
        }
        subSelector = subSelector.toLowerCase();

        const parent = elem.parentElement;

        if (parent) {
            const sameTagSiblings = parent.children;
            if (sameTagSiblings.length > 1) {
                let nameCount = 0;
                const index = [...sameTagSiblings].findIndex((child) => {
                    if (elem.localName === child.localName) {
                        nameCount++;
                    }
                    return child === elem;
                }) + 1;
                if (index > 1 && nameCount > 1) {
                    subSelector += ':nth-child(' + index + ')';
                }
            }
        }

        path = subSelector + (path ? '>' + path : '');
        elem = parent;
    }
    return path;
}

Solution 7 - Javascript

I found for my self some modified solution. I added to path selector #id, .className and cut the lenght of path to #id:

$.fn.extend({
            getSelectorPath: function () {
                var path,
                    node = this,
                    realNode,
                    name,
                    parent,
                    index,
                    sameTagSiblings,
                    allSiblings,
                    className,
                    classSelector,
                    nestingLevel = true;

                while (node.length && nestingLevel) {
                    realNode = node[0];
                    name = realNode.localName;

                    if (!name) break;

                    name = name.toLowerCase();
                    parent = node.parent();
                    sameTagSiblings = parent.children(name);

                    if (realNode.id) {
                        name += "#" + node[0].id;

                        nestingLevel = false;

                    } else if (realNode.className.length) {
                        className =  realNode.className.split(' ');
                        classSelector = '';

                        className.forEach(function (item) {
                            classSelector += '.' + item;
                        });

                        name += classSelector;

                    } else if (sameTagSiblings.length > 1) {
                        allSiblings = parent.children();
                        index = allSiblings.index(realNode) + 1;

                        if (index > 1) {
                            name += ':nth-child(' + index + ')';
                        }
                    }

                    path = name + (path ? '>' + path : '');
                    node = parent;
                }

                return path;
            }
        });

Solution 8 - Javascript

This answer does not satisfy the original question description, however it does answer the title question. I came to this question looking for a way to get a unique selector for an element but I didn't have a need for the selector to be valid between page-loads. So, my answer will not work between page-loads.

I feel like modifying the DOM is not idel, but it is a good way to build a selector that is unique without a tun of code. I got this idea after reading @Eli's answer:

Assign a custom attribute with a unique value.

$(element).attr('secondary_id', new Date().getTime())
var secondary_id = $(element).attr('secondary_id');

Then use that unique id to build a CSS Selector.

var selector = '[secondary_id='+secondary_id+']';

Then you have a selector that will select your element.

var found_again = $(selector);

And you many want to check to make sure there isn't already a secondary_id attribute on the element.

if ($(element).attr('secondary_id')) {
  $(element).attr('secondary_id', (new Date()).getTime());
}
var secondary_id = $(element).attr('secondary_id');

Putting it all together

$.fn.getSelector = function(){
  var e = $(this);

  // the `id` attribute *should* be unique.
  if (e.attr('id')) { return '#'+e.attr('id') }

  if (e.attr('secondary_id')) {
    return '[secondary_id='+e.attr('secondary_id')+']'
  }

  $(element).attr('secondary_id', (new Date()).getTime());

  return '[secondary_id='+e.attr('secondary_id')+']'
};

var selector = $('*').first().getSelector();

Solution 9 - Javascript

In case you have an identity attribute (for example id="something"), you should get the value of it like,

var selector = "[id='" + $(yourObject).attr("id") + "']";
console.log(selector); //=> [id='something']
console.log($(selector).length); //=> 1

In case you do not have an identity attribute and you want to get the selector of it, you can create an identity attribute. Something like the above,

var uuid = guid();
$(yourObject).attr("id", uuid); // Set the uuid as id of your object.

You can use your own guid method, or use the source code found in this so answer,

function guid() {
    function s4() {
        return Math.floor((1 + Math.random()) * 0x10000)
                .toString(16)
                .substring(1);
    }
    return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
            s4() + '-' + s4() + s4() + s4();
}

Solution 10 - Javascript

My Vanilla JavaScript function:

function getUniqueSelector( element ) {
    if (element.id) {
        return '#' + element.id;
    } else if (element.tagName === 'BODY') {
        return 'BODY';
    } else {
        return `${getUniqueSelector(element.parentElement)} > ${element.tagName}:nth-child(${myIndexOf(element)})`;
    }
}

function myIndexOf( element ) {
    let index = 1;
    // noinspection JSAssignmentUsedAsCondition
    while (element = element.previousElementSibling) index++;
    return index;
}

Solution 11 - Javascript

You may also have a look at findCssSelector. Code is in my other answer.

Solution 12 - Javascript

You could do something like this:

$(".track").click(function() {
  recordEvent($(this).attr("id"));
});

It attaches an onclick event handler to every object with the track class. Each time an object is clicked, its id is fed into the recordEvent() function. You could make this function record the time and id of each object or whatever you want.

Solution 13 - Javascript

$(document).ready(function() {
    $("*").click(function(e) {
        var path = [];
        $.each($(this).parents(), function(index, value) {
            var id = $(value).attr("id");
            var class = $(value).attr("class");
            var element = $(value).get(0).tagName
                path.push(element + (id.length > 0 ? " #" + id : (class.length > 0 ? " .": "") + class));
        });
        console.log(path.reverse().join(">"));
        return false;
    });
});

Working example: http://jsfiddle.net/peeter/YRmr5/

You'll probably run into issues when using the * selector (very slow) and stopping the event from bubbling up, but cannot really help there without more HTML code.

Solution 14 - Javascript

You could do something like that (untested)

function GetPathToElement(jElem)
{
   var tmpParent = jElem;
   var result = '';
   while(tmpParent != null)
   {
       var tagName = tmpParent.get().tagName;
       var className = tmpParent.get().className;
       var id = tmpParent.get().id;
       if( id != '') result = '#' + id + result;
       if( className !='') result = '.' + className + result;
       result = '>' + tagName + result;
       tmpParent = tmpParent.parent();
    }
    return result;
}

this function will save the "path" to the element, now to find the element again in the future it's gonna be nearly impossible the way html is because in this function i don't save the sibbling index of each element,i only save the id(s) and classes.

So unless each and every-element of your html document have an ID this approach won't work.

Solution 15 - Javascript

Getting the dom path using jquery and typescript functional programming

function elementDomPath( element: any, selectMany: boolean, depth: number ) {
        const elementType = element.get(0).tagName.toLowerCase();
        if (elementType === 'body') {
            return '';
        }

        const id          = element.attr('id');
        const className   = element.attr('class');
        const name = elementType + ((id && `#${id}`) || (className && `.${className.split(' ').filter((a: any) => a.trim().length)[0]}`) || '');

        const parent = elementType === 'html' ? undefined : element.parent();
        const index = (id || !parent || selectMany) ? '' : ':nth-child(' + (Array.from(element[0].parentNode.children).indexOf(element[0]) + 1) + ')';

        return !parent ? 'html' : (
            elementDomPath(parent, selectMany, depth + 1) +
            ' ' +
            name +
            index
        );
    }

Solution 16 - Javascript

Pass the js element (node) to this function.. working little bit.. try and post your comments

function getTargetElement_cleanSelector(element){
    let returnCssSelector = '';

    if(element != undefined){


        returnCssSelector += element.tagName //.toLowerCase()

        if(element.className != ''){
            returnCssSelector += ('.'+ element.className.split(' ').join('.')) 
        }

        if(element.id != ''){
            returnCssSelector += ( '#' + element.id ) 
        }

        if(document.querySelectorAll(returnCssSelector).length == 1){
            return returnCssSelector;
        }

        if(element.name != undefined && element.name.length > 0){
            returnCssSelector += ( '[name="'+ element.name +'"]' ) 
        }

        if(document.querySelectorAll(returnCssSelector).length == 1){
            return returnCssSelector;
        }

        console.log(returnCssSelector)

        let current_parent = element.parentNode;

        let unique_selector_for_parent = getTargetElement_cleanSelector(current_parent);

        returnCssSelector = ( unique_selector_for_parent + ' > ' + returnCssSelector )

        console.log(returnCssSelector)

        if(document.querySelectorAll(returnCssSelector).length == 1){
            return returnCssSelector;
        }

    }
    

    return returnCssSelector;
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAlpView Question on Stackoverflow
Solution 1 - JavascriptAlpView Answer on Stackoverflow
Solution 2 - JavascriptalgorhythmView Answer on Stackoverflow
Solution 3 - JavascriptEliView Answer on Stackoverflow
Solution 4 - JavascriptGary GreenView Answer on Stackoverflow
Solution 5 - JavascriptRaphael RafatpanahView Answer on Stackoverflow
Solution 6 - JavascriptBrett ZamirView Answer on Stackoverflow
Solution 7 - JavascriptRikki NikView Answer on Stackoverflow
Solution 8 - JavascriptNateView Answer on Stackoverflow
Solution 9 - JavascriptGeorgios SyngouroglouView Answer on Stackoverflow
Solution 10 - JavascriptDaniel De LeónView Answer on Stackoverflow
Solution 11 - JavascriptAshraf SabryView Answer on Stackoverflow
Solution 12 - JavascriptNick BruntView Answer on Stackoverflow
Solution 13 - JavascriptPeeterView Answer on Stackoverflow
Solution 14 - JavascriptJean-Philippe GireView Answer on Stackoverflow
Solution 15 - JavascriptNevo DavidView Answer on Stackoverflow
Solution 16 - JavascriptVijayakumar S BaskarView Answer on Stackoverflow