htmlstreamofficial / preline

Preline UI is an open-source set of prebuilt UI components based on the utility-first Tailwind CSS framework.
https://preline.co
Other
4.9k stars 309 forks source link

Advanced Select does not render conditionally in Next.js #350

Closed ahofmeister closed 1 month ago

ahofmeister commented 7 months ago

If rendered conditionally, the select is not rendered at all. I have checked the other issues, but could not find any specific related to this topic. If it is a duplicate, please close it :)

'use client';
import React, { useState } from 'react';
const Custom = () => {
  const [display, setDisplay] = useState(false);

  return (
    <div>
      <div onClick={() => setDisplay(true)}>display to true</div>
      {display && (
        <div>
          yes
          <select
            data-hs-select='{
  "placeholder": "Select option...",
  "toggleTag": "<button type=\"button\"></button>",
  "toggleClasses": "hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 relative py-3 px-4 pe-9 flex text-nowrap w-full cursor-pointer bg-white border border-gray-200 rounded-lg text-start text-sm focus:border-blue-500 focus:ring-blue-500 before:absolute before:inset-0 before:z-[1] dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400",
  "dropdownClasses": "mt-2 z-50 w-full max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700",
  "optionClasses": "py-2 px-4 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-none focus:bg-gray-100 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800",
  "optionTemplate": "<div class=\"flex justify-between items-center w-full\"><span data-title></span><span class=\"hidden hs-selected:block\"><svg class=\"flex-shrink-0 size-3.5 text-blue-600 dark:text-blue-500\" xmlns=\"http:.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>",
  "extraMarkup": "<div class=\"absolute top-1/2 end-3 -translate-y-1/2\"><svg class=\"flex-shrink-0 size-3.5 text-gray-500 dark:text-neutral-500\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 15 5 5 5-5\"/><path d=\"m7 9 5-5 5 5\"/></svg></div>"
}'
            className="hidden"
          >
            <option value="">Choose</option>
            <option>Name</option>
            <option>Email address</option>
            <option>Description</option>
            <option>User ID</option>
          </select>
        </div>
      )}
    </div>
  );
};

export default Custom;

I have created https://stackblitz.com/edit/stackblitz-starters-uc54d1 for it.

It seems, that the select is not rendered if the visibility is handled with a state update. It works fine, if the select is put outside of the condition.

gursesl commented 5 months ago

I am experiencing similar issues with Advanced Select and other pure JS components. Here's what might be happening:

If anyone can point out a good way to get around this, the Advanced Select and other JS components are amazingly-well designed.

olegpix commented 5 months ago

If rendered conditionally, the select is not rendered at all. I have checked the other issues, but could not find any specific related to this topic. If it is a duplicate, please close it :)

'use client';
import React, { useState } from 'react';
const Custom = () => {
  const [display, setDisplay] = useState(false);

  return (
    <div>
      <div onClick={() => setDisplay(true)}>display to true</div>
      {display && (
        <div>
          yes
          <select
            data-hs-select='{
  "placeholder": "Select option...",
  "toggleTag": "<button type=\"button\"></button>",
  "toggleClasses": "hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 relative py-3 px-4 pe-9 flex text-nowrap w-full cursor-pointer bg-white border border-gray-200 rounded-lg text-start text-sm focus:border-blue-500 focus:ring-blue-500 before:absolute before:inset-0 before:z-[1] dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400",
  "dropdownClasses": "mt-2 z-50 w-full max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700",
  "optionClasses": "py-2 px-4 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-none focus:bg-gray-100 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800",
  "optionTemplate": "<div class=\"flex justify-between items-center w-full\"><span data-title></span><span class=\"hidden hs-selected:block\"><svg class=\"flex-shrink-0 size-3.5 text-blue-600 dark:text-blue-500\" xmlns=\"http:.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>",
  "extraMarkup": "<div class=\"absolute top-1/2 end-3 -translate-y-1/2\"><svg class=\"flex-shrink-0 size-3.5 text-gray-500 dark:text-neutral-500\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 15 5 5 5-5\"/><path d=\"m7 9 5-5 5 5\"/></svg></div>"
}'
            className="hidden"
          >
            <option value="">Choose</option>
            <option>Name</option>
            <option>Email address</option>
            <option>Description</option>
            <option>User ID</option>
          </select>
        </div>
      )}
    </div>
  );
};

export default Custom;

I have created https://stackblitz.com/edit/stackblitz-starters-uc54d1 for it.

It seems, that the select is not rendered if the visibility is handled with a state update. It works fine, if the select is put outside of the condition.

Hi, in your case, the best way to use HSSelect is to initialize it using a constructor.

'use client';

import React, { useState, useEffect, useRef } from 'react';
import { HSSelect, ISelect, ISelectOptions } from 'preline/preline';

const Custom = () => {
  const selectRef = useRef<null | HTMLSelectElement>(null);
  const hsSelectRef = useRef<ISelect | null>(null);
  const [display, setDisplay] = useState(false);
  const [selectVal, setSelectVal] = useState('');
  const options: ISelectOptions = {
    placeholder: 'Select option...',
    dropdownSpace: 10,
    value: selectVal,
    toggleTag: '<button type="button"></button>',
    toggleClasses:
      'hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 relative py-3 px-4 pe-9 flex text-nowrap w-full cursor-pointer bg-white border border-gray-200 rounded-lg text-start text-sm focus:border-blue-500 focus:ring-blue-500 before:absolute before:inset-0 before:z-[1] dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400',
    dropdownClasses:
      'mt-2 z-50 w-full max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700',
    optionClasses:
      'py-2 px-4 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-none focus:bg-gray-100 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800',
    optionTemplate:
      '<div class="flex justify-between items-center w-full"><span data-title></span><span class="hidden hs-selected:block"><svg class="flex-shrink-0 size-3.5 text-blue-600 dark:text-blue-500" xmlns="http:.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span></div>',
    extraMarkup:
      '<div class="absolute top-1/2 end-3 -translate-y-1/2"><svg class="flex-shrink-0 size-3.5 text-gray-500 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg></div>',
  };

  useEffect(() => {
    if (display && selectRef.current) {
      hsSelectRef.current = new HSSelect(selectRef.current, options);
      (hsSelectRef.current as HSSelect).value = selectVal;
      (hsSelectRef.current as HSSelect).on('change', (payload: string) =>
        setSelectVal(payload)
      );

      return () => {
        if (hsSelectRef.current && (hsSelectRef.current as ISelect).destroy) {
          (hsSelectRef.current as ISelect).destroy();
          hsSelectRef.current = null;
        }
      };
    }
  }, [display]);

  return (
    <div>
      <button type="button" onClick={() => setDisplay(!display)}>
        Toggle select
      </button>
      {display && (
        <div>
          yes
          <select
            ref={selectRef}
            data-hs-select
            className="--prevent-on-load-init"
            style={{
              display: 'none',
            }}
          >
            <option value="">Choose</option>
            <option value="name">Name</option>
            <option value="emailAddress">Email address</option>
            <option value="description">Description</option>
            <option value="userId">User ID</option>
          </select>
        </div>
      )}
    </div>
  );
};

export default Custom;

I've added an example here https://stackblitz.com/edit/stackblitz-starters-nxlqpw

Summary:

  1. replace data parameters with js parameters
  2. initialize the element using the constructor
  3. use refs
  4. destroy the select when unmounting
ahofmeister commented 5 months ago

Hi @olegpix

I really appreciate your answer and time you put into it! We decided (not only because of this issue) to move forward with another library instead of Preline.

Not sure if you want to have this issue to remain open or not.

bluefire2121 commented 5 months ago

@olegpix There's an error in the stackblitz. https://stackblitz.com/edit/stackblitz-starters-uc54d1

ReferenceError: self is not defined

Any notes on how to resolve?

olegpix commented 5 months ago

@olegpix There's an error in the stackblitz. https://stackblitz.com/edit/stackblitz-starters-uc54d1

ReferenceError: self is not defined

Any notes on how to resolve?

I solved it this way. But this is not a subject of the opened issue.

import dynamic from 'next/dynamic';

const DynamicCustom = dynamic(() => import('@/components/custom'), {
  ssr: false,
});

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col">
      <DynamicCustom />
    </main>
  );
}
Gordi90 commented 5 months ago

The same happens when used in Angular and I suspect a similar reason as in Next.js. You have to call window.HSStaticMethods.autoInit(['select']); to make it work again.

koorukuroo commented 4 months ago

The same happens when used in Angular and I suspect a similar reason as in Next.js. You have to call window.HSStaticMethods.autoInit(['select']); to make it work again.

@Gordi90 provide the vivid hint. Just use autoInit();

  useEffect(() => {
    const loadPreline = async () => {
      await import("preline/preline");

      window.HSStaticMethods.autoInit();
    };

    loadPreline();
  }, [isLoaded]);