Home > Net >  jquery, if specific elements contains specific text, change the icon class before this
jquery, if specific elements contains specific text, change the icon class before this

Time:12-31

i have this html code:

<div >
online users
    <span >
         <i ></i>
         <span >Ster</span>
    </span>
    <span >
         <i ></i>
         <span >dimitris</span>
    </span>
    <!-- ... more users... -->
</div>

this code showing which users are online. I need a code to change the icons "i" before users names. Every user have specific icon.

i have try with this jquery code:

<script>
$(document).ready(function(){
    $('.online-users span.user-name').each(function(){
        $(this:contains('Ster')).siblings('i').toggleClass('fa-solid fa-user-gear');
        $(this:contains('dimitris')).siblings('i').toggleClass('fa-solid fa-user-pen');
    });
});
</script>

But nothing change. Any idea what is wrong with my code?

CodePudding user response:

The main error in your code was using a selector like this:

$(this:contains('Ster'))

You need to pass a string to the jQuery $ function if you want to return the result of a css selector.

I refactored better your code to achieve the desired result, so that there's a dedicated function to change the style, toggling those fontawesome css classes, of the <i> element next to an element containing the username you wish to style.

The selector I used to fetch the .user-name element containing a given string was:

$(`.online-users > .user-online > .user-name:contains('${username}')`)

That's a template string (and it's wrapped by upticks instead of single quotes or double quotes). It looks for the element .user-name in that defined hierarchy and containing the value hold in the username variable.

When document is ready, each username found gets styled like that.

...But the painful truth:

Your approach doesn't play well with every scenario. Consider if you have a list of users where there's one containing the other like: user and user2. In that case the :contains selector will hit twice on the same element and will screw the style because you were toggling the class and not just adding it.

EDIT: Later I better factored 4 modes to deal with the problem where one of them doesn't have issue with usernames sharing the same root.

$(document).ready(function(){
 toggleUsersIconStyle(4);
});

/**
 * Toggles the classes fa-solid and fa-user-gear to the <i> tags
 * matching this selector ran for each of the entry in the usernames list:
 * `.online-users > .user-online > .user-name:contains('${username}') i.fa`
 *
 * Notes: it's the easiest approach but it relies on :contains so it won't be an exact match.
 * Plus it loops the usernames list delegating the elements search to css
 */
function styleUsers(usernames){
  debugger;
  usernames.forEach(username=>{
    //in our dreams this selector should return one element only
    //but there's always the chance that multiple usernames have the same root
    $(`.online-users > .user-online > .user-name:contains('${username}')`)
      .each((i, el)=>{ $(el).siblings('i').toggleClass('fa-solid fa-user-gear') });
  });
}

/**
 * Toggles the classes fa-solid and fa-user-gear to the <i> tag
 * included in all the elements .online-users > .user-online
 * if the inner .user-name text content perfectly matches with any
 * included in the usernames list passed as argument 
 *
 * Notes: it loops through all the online element and makes the most accurate match
 */
function styleUsersAccurate(usernames){
  const users = $('.online-users > .user-online > .user-name');
  for(const user of users){
    const username = $(user).text();
    if(usernames.includes(username)){
      $(user).siblings('i.fa').toggleClass('fa-solid fa-user-gear');
    }
  }
}

/**
 * Toggles the classes fa-solid and fa-user-gear to the <i> tag
 * hold inside the .user-online element containing the passed username
 */
function styleUser(username){
 $(`.online-users > .user-online > .user-name:contains('${username}')`)
  .siblings('i')
  .toggleClass('fa-solid fa-user-gear');
}

function toggleUsersIconStyle(mode){

  if(mode === 1)
    //styleUsersAccurate approach
    styleUsersAccurate(['Ster','dimitris']);
    
  else if(mode === 2)
    //styleUsers approach
    styleUsers(['Ster','dimitris']);
    
  else if(mode === 3)
    //styleUser approach (styling only the username listed in the array)
    ['Ster','dimitris'].forEach((username)=>{
      styleUser(username);
    });
    
  else if(mode === 4)
    //styleUser approach (styling every single user existing in the dom)
    $('.online-users span.user-name').each((i, username)=>{
      styleUser( $(username).text() );
    });
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css"  />
<div >
  online users:
    <span >
         <i ></i>
         <span >Ster</span>
    </span>
    <span >
         <i ></i>
         <span >dimitris</span>
    </span>
    <!-- ... more users... -->
</div>

CodePudding user response:

The problem with your own code is:

$(this:contains('Ster'))

The :contains() jQuery selector works – as does a CSS Selector – as part of a string, whereas you've attempted to couple it to a DOM node (this), which results in a syntax error:

Uncaught SyntaxError: missing ) after argument list

You could work your way around this – if you wanted to – by using:

// we need to wrap this in a jQuery Object to enable
// the use of jQuery methods:
$(this)
  // and then we can use filter() to check if the
  // <string> is present within the element and
  // if so we can then chain further methods:
  .filter(':contains("<string>")')

Which gives the following approach:

$(document).ready(function() {
  $('.online-users span.user-name').each(function() {
    let userName = $(this).text();
    $(this).filter(':contains("dimitris")').prev('i').toggleClass('fa-solid fa-user-pen');
    $(this).filter(':contains("Ster")').prev('i').toggleClass('fa-solid fa-user-gear');
  });
});
*,
::before,
::after {
  box-sizing: border-box;
  font-size: 16px;
  margin: 0;
  padding: 0;
}

.online-users,
.user-online {
  border: 1px solid currentColor;
  display: grid;
  gap: 0.25em;
  padding-block: 0.25em;
  padding-inline: 0.5em;
}

.online-users {
  inline-size: fit-content;
  margin-block: 1em;
  margin-inline-start: 1em;
}

.user-online {
  grid-template-columns: repeat(2, max-content);
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css" integrity="sha512-MV7K8 y gLIBoVD59lQIYicR65iaqukzvf/nwasF0nqhPay5w/9lJmVM2hMDcnK1OnMGCdVK iQrJ7lzPJQd1w==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div >
  online users
  <span >
    <i ></i>
    <span >Ster</span>
  </span>
  <span >
    <i ></i>
    <span >dimitris</span>
  </span>
  <!-- ... more users... -->
</div>

JS Fiddle demo.

Or, instead of filter(), we could instead use the .is() method, which returns a Boolean (true/false) if the element matches, or does not match, the supplied CSS selector:

$(document).ready(function() {
  $('.online-users span.user-name').each(function() {
    let userName = $(this).text();
    if ($(this).is(':contains("dimitris")')) {
      $(this).prev('i').toggleClass('fa-solid fa-user-pen');
    } else if ($(this).is(':contains("Ster")')) {
      $(this).prev('i').toggleClass('fa-solid fa-user-gear');
    }
  });
});
*,
::before,
::after {
  box-sizing: border-box;
  font-size: 16px;
  margin: 0;
  padding: 0;
}

.online-users,
.user-online {
  border: 1px solid currentColor;
  display: grid;
  gap: 0.25em;
  padding-block: 0.25em;
  padding-inline: 0.5em;
}

.online-users {
  inline-size: fit-content;
  margin-block: 1em;
  margin-inline-start: 1em;
}

.user-online {
  grid-template-columns: repeat(2, max-content);
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css" integrity="sha512-MV7K8 y gLIBoVD59lQIYicR65iaqukzvf/nwasF0nqhPay5w/9lJmVM2hMDcnK1OnMGCdVK iQrJ7lzPJQd1w==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div >
  online users
  <span >
    <i ></i>
    <span >Ster</span>
  </span>
  <span >
    <i ></i>
    <span >dimitris</span>
  </span>
  <!-- ... more users... -->
</div>

JS Fiddle demo.

However it's worth noting that both of the previous approaches require you to code to explicitly check for each user-name, and hard-code those user-names and matching icons into the code. Not only does this require an update to the code each time a user is added, or removed and sets or changes their chosen icon the code itself has to check for every user; this leads to bloated code that's difficult to maintain.

Instead, I'd suggest the following approach, which requires one Object to be updated (which can be automatically generated on the back-end), and otherwise lets the code handle the checks automatically, this code has explanatory comments in the code:

// An object mapping user-names to the icons to show for
// the given user:
const userIcons = {
  Ster: 'fa-user-gear',
  dimitris: 'fa-user-pen'
}

$(document).ready(function() {
  // selecting the <span > elements within the
  // '.online-users' element, and iterating over that collection
  // with the each() method:
  $('.online-users span.user-name').each(function() {
    // caching the current user-name, from the text of the current
    // element and removing leading/trailing white-space with
    // String.prototype.trim():
    let userName = $(this).text().trim();

    // accessing the previous <i> element sibling:
    $(this).prev('i')
      // and toggling the listed class-names, which uses a
      // a template literal to always toggle the 'fa-solid'
      // class-name, and interpolating the result of the
      // userIcons[userName] expression:
      .toggleClass(`fa-solid ${userIcons[userName]}`);
  });
});
*,
::before,
::after {
  box-sizing: border-box;
  font-size: 16px;
  margin: 0;
  padding: 0;
}

.online-users,
.user-online {
  border: 1px solid currentColor;
  display: grid;
  gap: 0.25em;
  padding-block: 0.25em;
  padding-inline: 0.5em;
}

.online-users {
  inline-size: fit-content;
  margin-block: 1em;
  margin-inline-start: 1em;
}

.user-online {
  grid-template-columns: repeat(2, max-content);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css" integrity="sha512-MV7K8 y gLIBoVD59lQIYicR65iaqukzvf/nwasF0nqhPay5w/9lJmVM2hMDcnK1OnMGCdVK iQrJ7lzPJQd1w==" crossorigin="anonymous" referrerpolicy="no-referrer"
/>
<div >
  online users
  <span >
    <i ></i>
    <span >Ster</span>
  </span>
  <span >
    <i ></i>
    <span >dimitris</span>
  </span>
  <!-- ... more users... -->
</div>

JS Fiddle demo.

Or, in plain JavaScript:

// As before, an Object used to map the user-specific icon(s)
// to a named user:
const userIcons = {
    Ster: 'fa-user-gear',
  dimitris: 'fa-user-pen'
}

// an alternative to jQuery's $(document).ready():
window.addEventListener('DOMContentLoaded', ()=>{

  // retrieving a NodeList of all <span > elements
  // within the '.online-users' element, and iterating over that
  // NodeList with NodeList.prototype.forEach() and its anonymous
  // (Arrow) function:
    document.querySelectorAll('.online-users span.user-name').forEach(
    // passing in a reference to the current Node of the NodeList
    // over which we're iterating:
    (el)=>{
      // caching the user-name from the current node, and
      // removing leading/trailing white-space with String.prototype.trim():
        let userName = el.textContent.trim(),
          // caching a reference to the previousElementSibling node:
            previousSibling = el.previousElementSibling;

      // if the previous element matches the selector (here: 'i') supplied:
      if (previousSibling.matches('i')) {
        // we then use the Element.classList API to add the listed
        // comma-separated, class-names:
        previousSibling.classList.add('fa-solid', userIcons[userName]);
      }   
    });
});
*,
::before,
::after {
  box-sizing: border-box;
  font-size: 16px;
  margin: 0;
  padding: 0;
}

.online-users,
.user-online {
  border: 1px solid currentColor;
  display: grid;
  gap: 0.25em;
  padding-block: 0.25em;
  padding-inline: 0.5em;
}

.online-users {
  inline-size: fit-content;
  margin-block: 1em;
  margin-inline-start: 1em;
}

.user-online {
  grid-template-columns: repeat(2, max-content);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css" integrity="sha512-MV7K8 y gLIBoVD59lQIYicR65iaqukzvf/nwasF0nqhPay5w/9lJmVM2hMDcnK1OnMGCdVK iQrJ7lzPJQd1w==" crossorigin="anonymous" referrerpolicy="no-referrer"
/>
<div >
  online users
  <span >
    <i ></i>
    <span >Ster</span>
  </span>
  <span >
    <i ></i>
    <span >dimitris</span>
  </span>
  <!-- ... more users... -->
</div>

JS Fiddle demo.

References:

  • Related