preline icon indicating copy to clipboard operation
preline copied to clipboard

Advanced Select does not render conditionally in Next.js

Open ahofmeister opened this issue 1 year ago • 6 comments

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.

ahofmeister avatar Apr 23 '24 05:04 ahofmeister

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

  • Preline is a pure JS framework and as such, it is outside of React's virtual DOM (uncontrolled)
  • The Preline component gets rendered when the DOM is ready
  • Any subsequent changes to the React state only re-render that node of the DOM, not the entire tree
  • As such, the Preline pure JS component never gets re-rendered
  • @ahofmeister - in your case it remains hidden
  • It works [perfectly], except for any state changes. For example, if an error state must render the advanced select in a red border, that also doesn't work, since it requires a re-render based on a state change, which simply doesn't work.

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

gursesl avatar May 27 '24 21:05 gursesl

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

olegpix avatar May 30 '24 13:05 olegpix

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.

ahofmeister avatar May 31 '24 10:05 ahofmeister

@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?

bluefire2121 avatar May 31 '24 23:05 bluefire2121

@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>
  );
}

olegpix avatar Jun 01 '24 10:06 olegpix

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 avatar Jun 07 '24 15:06 Gordi90

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]);

koorukuroo avatar Jul 21 '24 12:07 koorukuroo