Как объединить разметку рейтинговой панели компонента звездного рейтинга с помощью цикла for?

У меня есть рейтинг для трех разных продуктов, и я пытаюсь отобразить звездный SVG на основе их рейтинга.
Например, у продукта-А рейтинг 3, я хочу отобразить звездное изображение 3 раза.
Максимальное количество звезд, которое они могут иметь, — 5.

Я написал такой код:

const ratingHandler = (productRate) => {
  for (let i = 0; i < productRate + 1; i++) {
    console.info(productRate);
    return `<img src = "assets/star.svg" alt = "rating-star" />`;
  }
};

Оценка продукта правильно показывает номер рейтинга в моем console.info, но рендеринг в цикле не выполняется на основе этого числа.

См. Останавливает ли возврат цикл?. Но вам следует подробно описать, что вы пытаетесь сделать, потому что это не return. Хотите создать массив? Строка? Добавить в DOM?

VLAZ 10.07.2024 18:12

Я пытаюсь визуализировать теги img на основе количества ставок, которые имеет каждый продукт. Если у продукта рейтинг 3, я хочу, чтобы тег img отображался 3 раза.

Afra 10.07.2024 18:14

Возможно, после чего-то вроде -> const ratingHandler = (productRate) => new Array(productRate).fill('<img src = "assets/star.svg" alt = "rating-star" />').join('')

Keith 10.07.2024 18:16

@Кейт, а может быть проще str.repeat(productRate)

VLAZ 10.07.2024 18:22

@VLAZ Старые привычки :), хотя они уже давно доступны..

Keith 10.07.2024 18:27

Реальная проблема ОП гораздо глубже. Звездный рейтинг следует рассматривать как компонент, абстракцию проблемы, которая в общих чертах реализуется один раз. Таким образом, мы стремимся к многоразовому компоненту, который можно легко адаптировать путем изменения значений атрибутов и изменений CSS. Его разметку можно использовать гибко, хотя она семантически более правильна, чем последовательность звездных изображений OP.

Peter Seliger 11.07.2024 09:35

@PeterSeliger Не совсем, вы, конечно, можете продвигать лучшие практики и возможность повторного использования компонентов, но ответы на SO должны быть прямыми и простыми. Сказав это, ваш ответ хорошо представлен, и я вижу много усилий, и я вижу, что другие пользователи, натыкающиеся на SO, могут найти это полезным, и по этой причине я буду голосовать... :)

Keith 11.07.2024 15:53
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
7
230
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Ответ принят как подходящий

Ваша функция завершается, как только она впервые достигает return. Перед возвратом вам необходимо создать полную строку:

const ratingHandler = (productRate) => {
  let stars = '';
  for (let i = 0; i < productRate + 1; i++) {
    stars += `<img src = "assets/star.svg" alt = "rating-star" />`;
  }
  return stars;
};

удивительный! Спасибо! это сработало! У меня есть еще один вопрос: для ставок меньше 5 я хочу добавить имя класса «.inactiveStar» к этому тегу изображения. Обычно, если у продукта рейтинг 4, img отображается 5 раз, но последний имеет имя класса .inactiveStar

Afra 10.07.2024 18:25

@Афра, это совсем другая проблема. Я бы посоветовал вам задать новый вопрос об этом вместо того, чтобы расширять этот.

Rory McCrossan 10.07.2024 18:27

@Афра, может быть, что-то вроде этого - вместо изображений используется Юникод, но вы поймете идею, просто добавьте свой класс в часть else.

Andy 10.07.2024 18:34

@Энди, это было очень полезно, спасибо!

Afra 10.07.2024 18:55

Для интереса: возможно, альтернативный (без цикла, используйте фон CSS для изображений) способ создания звезд из рейтинга:

[Редактирование]

const toBody = html => document.body.insertAdjacentHTML(`beforeend`, html);
document.addEventListener(`click`, handle);
  
[0, 4, 3, 1, 5, 2].forEach(v => toBody(rate(v)));

document.body.insertAdjacentHTML(
  `beforeend`,
  `<div class = "rateMe" data-maxrate = "10">
    ${rate(0, 10, true)}
    <div><button data-reset='1'>reset</button></div>
   </div> `
    
);

function rate(rating, n = 5) {
  return `<div data-nstars = "${n}" data-rating = "${rating}">
    ${`<span class = "star active"></span>`.repeat(rating)}${
      `<span class = "star inactive"></span>`.repeat(n-rating)}</div>`;
}

function handle(evt) {
  const rateElement = evt.target.closest(`.rateMe`);
  
  if (rateElement) {
    const maxRate = +rateElement.dataset.maxrate;
    const stars = rateElement.querySelectorAll(`.star`);
    rateElement.querySelector(`[data-rating]`).remove();
    const score = evt.target.dataset.reset ? 0 :
      [...stars].reduce((acc, s, i) => s === evt.target ? acc + i : acc, 1);
    return rateElement.insertAdjacentHTML(`afterBegin`, rate(score, maxRate));
  }
}
[data-rating]:before {
  content: attr(data-rating)'/'attr(data-nstars);
  color: green;
}

.star {
  background-repeat: no-repeat;
  background-size: cover;
  width: 16px;
  height: 16px;
  display: inline-block;
  margin-left: 3px;
}

.rateMe:before {
  content: 'Click to rate';
  display: block;
  margin: 1rem 0 0.2rem 0;
  font-weight: bold;
}

.rateMe div {
  cursor: pointer;
  display: inline-block;
}

.inactive {
  background-image: url('data:image/svg+xml,%3Csvg%20height%3D%2250px%22%20width%3D%2250px%22%20version%3D%221.1%22%20id%3D%22Capa_1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20viewBox%3D%220%200%2047.94%2047.94%22%20xml%3Aspace%3D%22preserve%22%20fill%3D%22%23000000%22%20stroke%3D%22%23000000%22%20stroke-width%3D%220.62322%22%3E%3Cg%20id%3D%22SVGRepo_bgCarrier%22%20stroke-width%3D%220%22%3E%3C%2Fg%3E%3Cg%20id%3D%22SVGRepo_tracerCarrier%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3C%2Fg%3E%3Cg%20id%3D%22SVGRepo_iconCarrier%22%3E%20%3Cpath%20style%3D%22fill%3A%23FFFFFF%3B%22%20d%3D%22M26.285%2C2.486l5.407%2C10.956c0.376%2C0.762%2C1.103%2C1.29%2C1.944%2C1.412l12.091%2C1.757%20c2.118%2C0.308%2C2.963%2C2.91%2C1.431%2C4.403l-8.749%2C8.528c-0.608%2C0.593-0.886%2C1.448-0.742%2C2.285l2.065%2C12.042%20c0.362%2C2.109-1.852%2C3.717-3.746%2C2.722l-10.814-5.685c-0.752-0.395-1.651-0.395-2.403%2C0l-10.814%2C5.685%20c-1.894%2C0.996-4.108-0.613-3.746-2.722l2.065-12.042c0.144-0.837-0.134-1.692-0.742-2.285l-8.749-8.528%20c-1.532-1.494-0.687-4.096%2C1.431-4.403l12.091-1.757c0.841-0.122%2C1.568-0.65%2C1.944-1.412l5.407-10.956%20C22.602%2C0.567%2C25.338%2C0.567%2C26.285%2C2.486z%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');
}

.active, .rateMe .star:hover {
  background-image: url('data:image/svg+xml,%3Csvg%20height%3D%2250px%22%20width%3D%2250px%22%20version%3D%221.1%22%20id%3D%22Capa_1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20viewBox%3D%220%200%2047.94%2047.94%22%20xml%3Aspace%3D%22preserve%22%20fill%3D%22%23000000%22%20stroke%3D%22%23000000%22%20stroke-width%3D%220.62322%22%3E%3Cg%20id%3D%22SVGRepo_bgCarrier%22%20stroke-width%3D%220%22%3E%3C%2Fg%3E%3Cg%20id%3D%22SVGRepo_tracerCarrier%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3C%2Fg%3E%3Cg%20id%3D%22SVGRepo_iconCarrier%22%3E%20%3Cpath%20style%3D%22fill%3A%23ED8A19%3B%22%20d%3D%22M26.285%2C2.486l5.407%2C10.956c0.376%2C0.762%2C1.103%2C1.29%2C1.944%2C1.412l12.091%2C1.757%20c2.118%2C0.308%2C2.963%2C2.91%2C1.431%2C4.403l-8.749%2C8.528c-0.608%2C0.593-0.886%2C1.448-0.742%2C2.285l2.065%2C12.042%20c0.362%2C2.109-1.852%2C3.717-3.746%2C2.722l-10.814-5.685c-0.752-0.395-1.651-0.395-2.403%2C0l-10.814%2C5.685%20c-1.894%2C0.996-4.108-0.613-3.746-2.722l2.065-12.042c0.144-0.837-0.134-1.692-0.742-2.285l-8.749-8.528%20c-1.532-1.494-0.687-4.096%2C1.431-4.403l12.091-1.757c0.841-0.122%2C1.568-0.65%2C1.944-1.412l5.407-10.956%20C22.602%2C0.567%2C25.338%2C0.567%2C26.285%2C2.486z%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');
}

Я тоже рискнул реализовать активный компонент Star-Rating, чтобы продемонстрировать, как, по моему мнению, может выглядеть подход к достаточно общему, доступному, настраиваемому, поддерживаемому и, следовательно, повторно используемому (веб-)компоненту.

Peter Seliger 22.07.2024 16:36

Обратите внимание на отличные комментарии @VLAZ по вопросу:

const ratingHandler = productRate =>
  '<img src = "assets/star.svg" alt = "rating-star" />'.repeat(productRate+1)

Если вы хотите быть крутым парнем в классе, и никто не должен понимать ваш код, но он все равно должен выполнять свою работу, это ваш вариант:

const ratingHandler = (productRate) => [undefined].concat(productRate ?? []).reduce((all) => all += '<img src = "assets/star.svg" alt = "rating-star" />', "");

Отблагодаришь позже!

Из моего комментария выше...

Реальная проблема ОП гораздо глубже. Звездный рейтинг следует рассматривать как компонент, абстракцию проблемы, которая в общих чертах реализуется один раз. Таким образом, мы стремимся к многоразовому компоненту, который можно легко адаптировать путем изменения значений атрибутов и изменений CSS. Его разметку можно использовать гибко, хотя она семантически более правильна, чем последовательность звездных изображений OP.

Первый шаг — придумать осмысленную разметку и немного CSS… пока без использования JavaScript.

[data-star-rating] {

  position: relative;
  /* display: inline-block; */
  display: block;
  /* margin: 0; */
  margin: 8px 0;

  > p,
  > span {
  
    display: inline-block;
    margin: 0 4px 0 0;

    &:after {
      content: ':'
    }
  }
  > output,
  > figcaption {

    position: relative;
    display: inline-block;
    margin: 0;
    color: transparent;

    &:after,
    &:before {

      position: absolute;
      left: 0;
      top: -8px;

      content: '';
      
      font-size: 1.3em;
      letter-spacing: 2px;
      -webkit-text-stroke: 1px black;
    }

    &:after {
      color: white;
    }
    &:before {
      z-index: 1;
      color: gold;
    }

    &[data-max-amount = "3"] {
      &:after {
        content: '★★★';
      }
    }
    &[data-max-amount = "4"] {
      &:after {
        content: '★★★★';
      }
    }
    &[data-max-amount = "5"] {
      &:after {
        content: '★★★★★';
      }
    }
    &[data-max-amount = "6"] {
      &:after {
        content: '★★★★★★';
      }
    }
    &[data-max-amount = "7"] {
      &:after {
        content: '★★★★★★★';
      }
    }

    &[data-amount = "1"] {
      &:before {
        content: '★';
      }
    }
    &[data-amount = "2"] {
      &:before {
        content: '★★';
      }
    }
    &[data-amount = "3"] {
      &:before {
        content: '★★★';
      }
    }
    &[data-amount = "4"] {
      &:before {
        content: '★★★★';
      }
    }
    &[data-amount = "5"] {
      &:before {
        content: '★★★★★';
      }
    }
    &[data-amount = "6"] {
      &:before {
        content: '★★★★★★';
      }
    }
    &[data-amount = "7"] {
      &:before {
        content: '★★★★★★★';
      }
    }
  }
}
<figure data-star-rating>
  <p>Rating</p>
  <figcaption data-amount = "4" data-max-amount = "7" title = "4 of 7 stars">
    4 of 7 stars
  </figcaption>
</figure>

<label data-star-rating>
  <span>Rating</span>
  <output data-amount = "2" data-max-amount = "5" title = "2 of 5 stars">
    2 of 5 stars
  </output>
</label>

<figure data-star-rating>
  <p>Rating</p>
  <figcaption data-amount = "1" data-max-amount = "4" title = "1 of 4 stars">
    1 of 4 stars
  </figcaption>
</figure>

<label data-star-rating>
  <span>Rating</span>
  <output data-amount = "3" data-max-amount = "3" title = "3 of 3 stars">
    3 of 3 stars
  </output>
</label>

<figure data-star-rating>
  <p>Rating</p>
  <figcaption data-amount = "5" data-max-amount = "6" title = "5 of 6 stars">
    5 of 6 stars
  </figcaption>
</figure>

Следующая итерация обеспечивает еще лучшую разметку с точки зрения использования (копию рейтинга можно опустить/показать), семантики и доступности. Правила показанного выше CSS были адаптированы в соответствии с этой новой структурой HTML. Сверху находится JavaScript, который реализует star-ratingвеб-компонент.

Реализация представляет собой автономный пользовательский элемент, который должен наследовать от базового класса HTML-элемента HTMLElement. Это связано с отсутствием поддержки Apple настраиваемых встроенных элементов, которые могут наследовать от стандартных элементов HTML.

Семантический разрыв, связанный с невозможностью использования <figure is = "star-rating"/>, можно обойти, используя подобную роль арии <star-rating aria-role = "figure" />.

В следующем примере кода представлен дополнительный скрипт, который позволяет манипулировать каждым компонентом с рейтингом, чтобы доказать концепцию компонента и правильную реализацию.

Каждый компонент звездного рейтинга можно использовать путем предоставления и/или мутации/изменения двух значений пользовательских данных-* атрибута. Это так просто.

function parseDataKey(attrName) {
  return attrName
    .replace(/^data-/, '')
    .replace(/-(\p{Ll})/gu, (_, char) => char.toUpperCase());
}

function getApprovedAmountValue(initialData, dataKey, latestValue) {
  const initialValue = initialData[dataKey];

  latestValue = Number(latestValue);
  latestValue = Number.isFinite(latestValue) ? latestValue : initialValue;

  return String(
    approvedValue = (dataKey === 'maxAmount')
      && Math.max(3, Math.min(latestValue, initialValue))
      || Math.max(0, Math.min(latestValue, initialData['maxAmount'] ?? latestValue))
  );
}


class StarRating extends HTMLElement {
  static observedAttributes = ['data-amount', 'data-max-amount'];

  #initialData = {};
  #captionNode;

  constructor() {
    super();

    Object.assign(this.#initialData, {
      amount: null,
      maxAmount: null,
    })
    this.#captionNode = this.querySelector('[data-template-target]');
  }
  renderCaption() {
    const { dataset: { template, amount, maxAmount, } } = this;
    
    const ratingCopy = template
      .replace(/\${\samount\s\}/g, amount)
      .replace(/\${\smaxAmount\s\}/g, maxAmount);

    this.#captionNode.textContent = ratingCopy;
    this.title = ratingCopy;
  }

  attributeChangedCallback(attrName, recentValue, currentValue) {
    const dataKey = parseDataKey(attrName);

    if (recentValue === null) {
      this.#initialData[dataKey] = currentValue;
    }
    const approvedValue =
      getApprovedAmountValue(this.#initialData, dataKey, currentValue);

    if (approvedValue !== currentValue) {

      // set correct value and indirectly trigger method execution again .
      this.dataset[dataKey] = approvedValue;

    } else {

      // rendering of valid/sanitized value changes only.
      this.renderCaption();
    }    
  }
}
customElements.define("star-rating", StarRating);
star-rating/*, [is = "star-rating"]*/ {

  position: relative;
  display: block;
  margin: 12px 0;

  > figcaption:has(> [data-template-target]) {

    position: relative;
    float: left;
    margin: 0;
    padding: 1px 4px 0 0;

    > [data-template-target] {

      overflow: hidden;
      position: absolute;
      width: 0;
      
      &.visible {
        overflow: unset;
        position: unset;
        width: unset;
      }
    }
  }
  > [data-figure] {

    display: inline-block;
    position: relative;
    top: -1.3em;

    &:after,
    &:before {

      position: absolute;
      left: 0;
      top: 0;

      content: '';
      
      font-size: 1.3em;
      letter-spacing: 2px;
      -webkit-text-stroke: 1px black;
    }

    &:after {
      color: white;
    }
    &:before {
      z-index: 1;
      color: gold;
    }
  }

  &[data-max-amount = "3"] {
    > [data-figure] {
      &:after {
        content: '★★★';
      }
    }
  }
  &[data-max-amount = "4"] {
    > [data-figure] {
      &:after {
        content: '★★★★';
      }
    }
  }
  &[data-max-amount = "5"] {
    > [data-figure] {
      &:after {
        content: '★★★★★';
      }
    }
  }
  &[data-max-amount = "6"] {
    > [data-figure] {
      &:after {
        content: '★★★★★★';
      }
    }
  }
  &[data-max-amount = "7"] {
    > [data-figure] {
      &:after {
        content: '★★★★★★★';
      }
    }
  }

  &[data-amount = "1"] {
    > [data-figure] {
      &:before {
        content: '★';
      }
    }
  }
  &[data-amount = "2"] {
    > [data-figure] {
      &:before {
        content: '★★';
      }
    }
  }
  &[data-amount = "3"] {
    > [data-figure] {
      &:before {
        content: '★★★';
      }
    }
  }
  &[data-amount = "4"] {
    > [data-figure] {
      &:before {
        content: '★★★★';
      }
    }
  }
  &[data-amount = "5"] {
    > [data-figure] {
      &:before {
        content: '★★★★★';
      }
    }
  }
  &[data-amount = "6"] {
    > [data-figure] {
      &:before {
        content: '★★★★★★';
      }
    }
  }
  &[data-amount = "7"] {
    > [data-figure] {
      &:before {
        content: '★★★★★★★';
      }
    }
  }
}

fieldset { margin: 24px 0 0 0; label { margin: 0 8px 0 0; } }
<star-rating aria-role = "figure"

  data-amount = "1"
  data-max-amount = "4"
  data-template = "${ amount } of ${ maxAmount } stars">

  <figcaption>
    Rating:
    <span data-template-target class = "visible">-- of -- stars</span> /
  </figcaption>

  <div data-figure></div>
</star-rating>


<star-rating aria-role = "figure"

  data-amount = "3"
  data-max-amount = "3"
  data-template = "${ amount } of ${ maxAmount } stars">

  <figcaption>
    Rating
    ... <span data-template-target class = "visible">-- of -- stars</span> ...
  </figcaption>

  <div data-figure></div>
</star-rating>


<star-rating aria-role = "figure"

  data-amount = "5"
  data-max-amount = "7"
  data-template = "${ amount } of ${ maxAmount } stars">

  <figcaption>
    Rating:
    <span data-template-target>-- of -- stars</span>
  </figcaption>

  <div data-figure></div>
</star-rating>


<fieldset>
  <legend>Change Star Ratings</legend>

  <label>
    <span>1st Rating</span>
    <select data-idx = "0">
      <option value = "0">none</option>
      <option value = "1" selected>★</option>
      <option value = "2">★★</option>
      <option value = "3">★★★</option>
      <option value = "4">★★★★</option>
    </select>
  </label>

  <label>
    <span>2nd Rating</span>
    <select data-idx = "1">
      <option value = "0">none</option>
      <option value = "1">★</option>
      <option value = "2">★★</option>
      <option value = "3" selected>★★★</option>
    </select>
  </label>

  <label>
    <span>3rd Rating</span>
    <select data-idx = "2">
      <option value = "0">none</option>
      <option value = "1">★</option>
      <option value = "2">★★</option>
      <option value = "3">★★★</option>
      <option value = "4">★★★★</option>
      <option value = "5" selected>★★★★★</option>
      <option value = "6">★★★★★★</option>
      <option value = "7">★★★★★★★</option>
    </select>
  </label>
</fieldset>

<script>
document
  .querySelector('fieldset')
  .addEventListener('change', ({ target }) => {
    document
      .querySelectorAll('star-rating')
      .item(target.dataset.idx)
      .dataset.amount = target.value;  
  });
</script>

Редактировать... особенно ориентируясь на поздние вклады @KooiInc, как его последний предоставленный пример кода в его собственном сообщении, так и его комментарий к этому моему сообщению...

@KooiInc ... да, приведенный выше пример реализует исключительно пассивный рейтинг. А поскольку он служит цели «только для отображения», можно сохранить семантически простую разметку, например, базовую <figure/>. Активный звездный компонент должен семантически следовать за <fieldset/>, который окружает радиогруппу. — Питер Селигер

Я создал версию веб-компонента , реализующую активный рейтинг . Он использует (синтаксис, не относящийся к классу) пользовательский модуль веб-компонента, который я создал ранее. – КоойИнк

А что касается последней правки @KooiInc, где он заявляет...

Вдохновленный ответом Питера Селигера, я создал (на мой взгляд, менее запутанный) веб-компонент для «ранжирования звезд».

... Я тоже рискнул привести еще один пример кода, где реализация активного компонента звездного рейтинга должна продемонстрировать то, что я имею в виду...

  • разметка компонента по умолчанию должна быть максимально семантически корректной и доступной.

  • разметка компонента может даже быть взаимозаменяемой (как показано на примере пассивного звездного рейтинга «только для отображения»).

  • Исходное состояние компонентов, данные и отображение, должно настраиваться с помощью атрибутов data-* и/или пользовательских атрибутов компонента.

  • JavaScript используется исключительно для управления инициализацией компонента, его взаимодействием с пользовательским интерфейсом и изменениями его состояния (но не для управления его макетом/дизайном).

  • CSS должен охватывать все остальные аспекты компонента, связанные с пользовательским интерфейсом.

В следующем примере показаны 3 различных компонента Star-Rating с навигацией по вкладкам. А четвертая радиогруппа без стиля и без JS, включающая набор полей, доказывает, что разметка HTML-структуры компонента может быть написана пригодным для использования и повторного использования способом, даже при отсутствии стилей и сценариев, особенно когда речь идет о с (основанными на) элементами управления формой.

function getValidatedInitialData(component) {

  const maxScoreUpperLimit = 13;
  const maxScoreLowerLimit = 3;

  const maxScoreDefault = 5;
  const scoreDefault = 0;

  let maxScore = Number(component.getAttribute('max-score') ?? maxScoreDefault);
  let score = Number(component.getAttribute('score') ?? scoreDefault);

  maxScore = Number.isFinite(maxScore)
    ? Math.max(maxScoreLowerLimit, Math.min(maxScoreUpperLimit, maxScore))
    : maxScoreDefault;

  return {
    maxScore,
    score: Number.isFinite(score)
      ? Math.max(0, Math.min(score, maxScore))
      : scoreDefault,
  };
}


function handleScoreItemStateChange(evt, nodes, state) {
  const { currentTarget: component, target: inputControl } = evt;

  const { scoreTarget, summaryTarget } = nodes;
  const { maxScore, summary } = state;

  // console.info('handleStateChange');

  const controlList = [
    ...scoreTarget.querySelectorAll(`[type = "${ inputControl.type }"]`)
  ];
  const selectedIndex = controlList.indexOf(inputControl);

  state.selectedIndex = selectedIndex;
  state.score = selectedIndex + 1;

  inputControl.value = state.score;

  if (summaryTarget) {
    summaryTarget[summary.attrName] = summary.value = summary.literal

      .replace(/\${\sscore\s\}/g, state.score)
      .replace(/\${\smaxScore\s\}/g, maxScore);
  }
}

function renderSelector(component, nodes, state) {

  const { scoreTemplate, scoreTarget, summaryTarget } = nodes;
  const { maxScore, score, summary } = state;

  const fragment = document.createDocumentFragment();

  Array
    .from(
      { length: maxScore }, () => scoreTemplate.content.cloneNode(true),
    )
    .forEach((node, idx) => {
      const scoreValue = idx + 1;

      const labelTarget = node.querySelector('[data-label][data-literal]');
      const inputControl = node.querySelector('[type = "radio"], [type = "checkbox"]');

      if (labelTarget) {
        labelTarget.textContent = (labelTarget.dataset.literal ?? '')

          .replace(/\${\sscore\s\}/g, scoreValue)
          .replace(/\${\smaxScore\s\}/g, maxScore);

        Reflect.deleteProperty(labelTarget.dataset, 'literal');
      }
      inputControl.value = scoreValue;

      if (scoreValue === score) {

        state.selectedIndex = idx;
        state.score = scoreValue;

        inputControl.checked = true;
      }
      fragment.appendChild(node);
    });

  if (summaryTarget) {
    summaryTarget[summary.attrName] = summary.value = summary.literal

      .replace(/\${\sscore\s\}/g, state.score)
      .replace(/\${\smaxScore\s\}/g, maxScore);
  }
  Reflect.deleteProperty(nodes, 'scoreTemplate');
  scoreTemplate.remove();

  scoreTarget.appendChild(fragment);
}

function initializeSelector(component, nodes, state) {

  const { maxScore, score } = getValidatedInitialData(component);
  const { summaryTarget } = nodes;

  state.maxScore = maxScore;
  state.score = score;

  component.removeAttribute('max-score');
  component.removeAttribute('score');

  if (summaryTarget) {
    const { summary } = state;

    summary.attrName = ('value' in summaryTarget) && 'value' || 'textContent';
    summary.literal = summaryTarget.dataset.literal ?? '';

    Reflect.deleteProperty(summaryTarget.dataset, 'literal');
  }
  component.addEventListener('change', evt => handleScoreItemStateChange(evt, nodes, state));

  renderSelector(component, nodes, state);
}


class StarRatingSelector extends HTMLElement {

  #state = {
    maxScore: null,
    score: null,
    summary: {
      attrName: null,
      literal: null,
      value: null,
    },
    selectedIndex: -1,
  };
  #nodes = {};

  constructor() {
    super();

    Object.assign(this.#nodes, {
      scoreTemplate: this.querySelector('template[data-score-item]'),
      scoreTarget: this.querySelector('[data-score-target]'),
      summaryTarget: this.querySelector('[data-summary][data-literal]'),
    });
  }

  get selectedIndex() {
    return this.#state.selectedIndex;
  }
  get score() {
    return this.#state.score;
  }
  get maxScore() {
    return this.#state.maxScore;
  }
  get summary() {
    return this.#state.summary.value;
  }

  connectedCallback() {
    initializeSelector(this, this.#nodes, this.#state);
  }
}
customElements.define('star-rating-selector', StarRatingSelector);
star-rating-selector/*,
[is = "star-rating-selector"]*/ {


  position: relative;
  display: block;
  margin: 12px 0;

  &:has(> fieldset) {

    margin-top: 16px;
    padding: 0 0 4px 0;

    border: 1px solid black;
    border-top: none;
  } 
  > fieldset {

    position: relative;
    top: -.55em;
    margin: 0 0 -1.4em 0;
    border: none;
    border-top: 1px solid black;
  }


  > [data-summary] {

    overflow: hidden;
    display: block;
    width: 0;
    height: 0;
    margin: 0;
    padding: 0;

    &.visible {
      overflow: unset;
      width: unset;
      height: unset;
      margin: 0 0 6px 0;
      padding: 0 16px;
    }
  }
  [data-score-target] {

    display: inline-block;
    margin: 0 0 10px 0;
    padding: 0;

    [data-score-item] {

      position: relative;
      display: inline-block;
      margin: 0 -2px;

      > [data-label] {

        overflow: hidden;
        float: left;
        width: 0;
        height: 0;
      }
      > input {

        position: absolute;
        left: 8px;
        top: 10px;
        width: 0;
        height: 0;
      }
      &::after {

        overflow: hidden;
        position: relative;
        left: 0;
        top: 1px;
        padding: 1px 2px 0 2px;

        content: '★';

        font-size: 1.8em;
        line-height: 1em;

        color: white;
        -webkit-text-stroke: 1px black;
      }
      &:focus-within {
        &::after {
          -webkit-text-stroke: 3px blue;
        }          
      }
      &:hover {
        &::after {
          -webkit-text-stroke: 3px purple;
        }
        cursor: pointer;
      }
      &:has(:checked) {
        &::after {
          color: gold;
        }
      }
    }
    &:has(:checked) {

      [data-score-item] {
        &::after {
          color: gold;
        }
        &:has(:checked) ~ [data-score-item] {
          &::after {
            color: white;
          }
        }
      }
    }
    &:hover {

      [data-score-item] {
        &::after {
          color: yellow!important;
        }
        &:hover ~ [data-score-item] {
          &::after {
            color: white!important;
          }
        }
      }
    }
  }
}
details { padding: 4px 0 8px 0; }
summary { padding: 0 0 4px 0; }
<star-rating-selector aria-label = "Star Rating Selector" role = "group" max-score = "4">
  <template data-score-item>
    <label data-score-item>

      <span data-label data-literal = "Rate with ${ score } of ${ maxScore } stars.">
        rate -- of -- stars
      </span>
      <input type = "radio" value = "" name = "out-of-four" />

    </label>
  </template>

  <fieldset>
    <legend>Please provide a score</legend>
    <div data-score-target></div>
  </fieldset>

  <output data-summary data-literal = "The current rating is ${ score } of ${ maxScore } stars." class = "visible">
    -- of -- stars selected
  </output>
</star-rating-selector>


<star-rating-selector aria-label = "Star Rating Selector" role = "group" score = "3" max-score = "3">
  <template data-score-item>
    <label data-score-item>

      <span data-label data-literal = "Rate with ${ score } of ${ maxScore } stars.">
        rate -- of -- stars
      </span>
      <input type = "radio" value = "" name = "out-of-three" />

    </label>
  </template>

  <fieldset>
    <legend>Please provide your rating</legend>
    <div data-score-target></div>
  </fieldset>

  <output data-summary data-literal = "${ score } of ${ maxScore } stars selected." class = "visible">
    -- of -- stars selected
  </output>
</star-rating-selector>


<star-rating-selector aria-label = "Star Rating Selector" role = "group" score = "5" max-score = "7">
  <template data-score-item>
    <label data-score-item>

      <span data-label data-literal = "Rate with ${ score } of ${ maxScore } stars.">
        rate -- of -- stars
      </span>
      <input type = "radio" value = "" name = "out-of-seven" />

    </label>
  </template>

  <fieldset>
    <legend>Give some stars</legend>
    <div data-score-target></div>
  </fieldset>

  <output data-summary data-literal = "${ score } of ${ maxScore } stars selected.">
    -- of -- stars selected
  </output>
</star-rating-selector>


<details>
  <summary>For Comparison Reason ...</summary>
  ... the CSS and JavaScript free base structure of any of the above "Star Rating" Components:
</details>


<fieldset aria-label = "Star Rating Selector">
  <legend>Rate with ...</legend>

    <label>
      <span>
        1 of 5 stars
      </span>
      <input type = "radio" value = "" name = "out-of-five" />
    </label>

    <label>
      <span>
        2 of 5 stars
      </span>
      <input type = "radio" value = "" name = "out-of-five" />
    </label>

    <label>
      <span>
        3 of 5 stars
      </span>
      <input type = "radio" value = "" name = "out-of-five" />
    </label>

    <label>
      <span>
        4 of 5 stars
      </span>
      <input type = "radio" value = "" name = "out-of-five" checked />
    </label>

    <label>
      <span>
        5 of 5 stars
      </span>
      <input type = "radio" value = "" name = "out-of-five" />
    </label>
</fieldset>

Хорошая идея, хотя и немного запутанная, имхо. Недостатком здесь может быть то, что пользователь не может оценить, просто щелкнув одну из звездочек.

KooiInc 16.07.2024 16:08

@KooiInc ... да, приведенный выше пример реализует исключительно пассивный рейтинг. А поскольку он служит цели «только для отображения», можно сохранить семантически простую разметку, например, базовую <figure/>. Активный звездный компонент должен семантически следовать за <fieldset/>, который окружает радиогруппу.

Peter Seliger 16.07.2024 20:14

Я создал версию веб-компонента , реализующую активный рейтинг . Он использует (синтаксис, не относящийся к классу) пользовательский модуль веб-компонента, который я создал ранее.

KooiInc 17.07.2024 14:32

@KooiInc ... интересно, мне это нравится. Преимущества подхода «без класса», который абстрагирует начальную загрузку / шаблонный код компонента класса, могут заключаться в том, что разработка будет направлена ​​на императивный стиль, основанный на функциях, где необходимо передавать все необходимые ссылки, данные и события. . Таким образом, это не позволяет разработчикам, не имеющим опыта объектно-ориентированного проектирования, возиться с кодом из-за неправильного выбора дизайна и даже худших реализаций.

Peter Seliger 17.07.2024 22:20

Другие вопросы по теме