You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SolidJS(TypeScript)点击外部切换组件状态失效问题排查

SolidJS点击外部切换组件状态问题排查与修复

问题背景

正在学习SolidJS框架,尝试通过自定义指令实现点击组件外部时切换组件状态,但功能失效。同时尝试使用ref的方案也遇到TypeScript错误,需要技术支持。


一、自定义指令方案代码及问题

组件代码

import { Component, createSignal, ParentComponent, Show } from 'solid-js';
import clickOutside from '../directives/click-outside';

clickOutside;

const EditorBlockMenu: Component<{fun: () => false}> = (prop) => {
    return (
        <div class='editor-block-menu' use:clickOutside={prop.fun}>
            <div><button>Delete</button></div>
            <div><button>Move Up</button></div>
            <div><button>Move Down</button></div>
        </div>
    )
}

const EditorBlockBtn: ParentComponent = () => {
    const [toggle, setToggle] = createSignal(false)
    const isToggle = () => setToggle(!toggle())

    return (
        <div classList={{'editor-block-btn': !toggle()}}>
            <button onClick={isToggle}>
                
            </button>
            <Show
                when={toggle()}
                fallback={<></>}
            >
                <EditorBlockMenu fun={() => setToggle(false)}/>
            </Show>
        </div>
    )
}

export default EditorBlockBtn;

自定义指令代码

import { onCleanup } from "solid-js";

// https://www.solidjs.com/tutorial/bindings_directives?solved
// need fix
export default function clickOutside(el:Element, accessor: () => false) {
  const onClick = (e: Event) => 
    !el.contains(e.target as Node) && accessor();
  document.body.addEventListener("click", onClick);
  onCleanup(() => document.body.removeEventListener("click", onClick));
}

declare module "solid-js" {
  namespace JSX {
    interface Directives {
      clickOutside: () => false;
    }
  }
}

推测setToggle()函数在fun={}属性中无法正常工作,对TypeScript类型定义不太熟悉,需要帮助。


二、Ref方案代码及错误

尝试用ref实现功能,但出现TypeScript错误,代码如下:

const EditorBlockMenu: Component = () => {
    return (
        <div class='editor-block-menu'>
            <div><button>Delete</button></div>
            <div><button>Move Up</button></div>
            <div><button>Move Down</button></div>
        </div>
    )
}

const EditorBlockBtn: ParentComponent = () => {
    const [toggle, setToggle] = createSignal(false)
    const isToggle = () => setToggle(!toggle())
    let ref: HTMLDivElement

    const handleClick = (event: MouseEvent) => {
        if(!ref.contains(event.target as Node)) { // 不加`as Node`会红标报错
          setToggle(false)
        }
      }
    
      onMount(() => {
        document.addEventListener('click', handleClick);
      })
      
      onCleanup(() => {
        document.removeEventListener('click', handleClick);
      })

    return (
        <div classList={{'editor-block-btn': !toggle()}}>
            <button onClick={isToggle}>
           
            </button>
            <Show
                when={toggle()}
                fallback={<></>}
            >
                <EditorBlockMenu ref={ref!}/>
            </Show>
        </div>
    )
}

export default EditorBlockBtn;

错误提示:
Type '{ ref: HTMLDivElement; }' is not assignable to type 'IntrinsicAttributes & { children?: any; }'. Property 'ref' does not exist on type 'IntrinsicAttributes & { children?: any; }'.


解决方案

1. 修复自定义指令方案

问题根源在于TypeScript类型定义错误,以及缺少事件冒泡阻止逻辑:

  • 指令接收的回调函数类型应为无返回值的函数,而非() => false
  • 点击组件内部时需阻止触发外部关闭逻辑,同时点击显示按钮时要阻止冒泡避免立即关闭

修改后的自定义指令代码

import { onCleanup } from "solid-js";

export default function clickOutside(el: Element, accessor: () => () => void) {
  const handler = accessor();
  
  const onClick = (e: MouseEvent) => {
    // 点击当前元素内部时不执行回调
    if (el.contains(e.target as Node)) return;
    handler();
  };

  document.body.addEventListener("click", onClick);
  onCleanup(() => document.body.removeEventListener("click", onClick));
}

declare module "solid-js" {
  namespace JSX {
    interface Directives {
      clickOutside: () => void;
    }
  }
}

修改后的组件代码关键部分

// 修正EditorBlockMenu的props类型
const EditorBlockMenu: Component<{fun: () => void}> = (prop) => {
    return (
        <div class='editor-block-menu' use:clickOutside={prop.fun}>
            <div><button>Delete</button></div>
            <div><button>Move Up</button></div>
            <div><button>Move Down</button></div>
        </div>
    )
}

// 修改按钮点击事件,阻止冒泡
const isToggle = (e: MouseEvent) => {
  e.stopPropagation();
  setToggle(!toggle())
}

// 按钮绑定修改后的事件
<button onClick={isToggle}>...</button>

2. 修复Ref方案

错误原因是EditorBlockMenu组件未定义ref属性,需调整组件接收ref并修正空值类型:

修改后的EditorBlockMenu组件

import { Component, mergeProps } from 'solid-js';

const EditorBlockMenu: Component<{ ref?: HTMLDivElement }> = (props) => {
    const merged = mergeProps({ ref: undefined }, props);
    return (
        <div ref={merged.ref} class='editor-block-menu'>
            <div><button>Delete</button></div>
            <div><button>Move Up</button></div>
            <div><button>Move Down</button></div>
        </div>
    )
}

修改后的EditorBlockBtn关键部分

// 允许ref为undefined,避免空值报错
let ref: HTMLDivElement | undefined;

// 同样给按钮点击事件添加阻止冒泡
const isToggle = (e: MouseEvent) => {
  e.stopPropagation();
  setToggle(!toggle())
}

内容的提问来源于stack exchange,提问作者Pahan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 10:30:05