我正在使用ClojureScript和Reagent处理树形控件。可以用作文件系统导航器,主题导航器,大纲视图,等。
在选择并编辑大纲中的标题时,点击Return
时的传统行为是创建新标题(子项或同级项,取决于标题的展开状态以及是否已子级或子级),然后将其聚焦,使其可以进行编辑。我的控件可以正确地完成所有操作,除非编辑组中的最后一个同级。
在有问题的情况下,标题是按预期方式创建的,但聚焦于新控件将失败。
我使用MCVE创建了一个figwheel
template。
lein new figwheel test-reagent-vector -- --reagent
这是一个显示问题的清单。
(ns test-reagent-vector.core
(:require [clojure.string :as s]
[reagent.core :as r]))
(def ^{:constant true} topic-separator \u02D1)
(def empty-test-topic {:topic "Empty Test Topic"})
(defonce global-state-with-hierarchy
(r/atom {:name "Global Application State, Inc."
:data {:one "one" :two 2 :three [3]}
:tree [{:topic "First Headline"}
{:topic "Middle Headline"}
{:topic "Last Headline"}]}))
(defn get-element-by-id
[id]
(.getElementById js/document id))
(defn event->target-element
[evt]
(.-target evt))
(defn event->target-value
[evt]
(.-value (event->target-element evt)))
(defn swap-style-property
"Swap the specified style settings for the two elements."
[first-id second-id property]
(let [style-declaration-of-first (.-style (get-element-by-id first-id))
style-declaration-of-second (.-style (get-element-by-id second-id))
value-of-first (.getPropertyValue style-declaration-of-first property)
value-of-second (.getPropertyValue style-declaration-of-second property)]
(.setProperty style-declaration-of-first property value-of-second)
(.setProperty style-declaration-of-second property value-of-first)))
(defn swap-display-properties
"Swap the display style properties for the two elements."
[first-id second-id]
(swap-style-property first-id second-id "display"))
;;------------------------------------------------------------------------------
;; Vector-related manipulations.
(defn delete-at
"Remove the nth element from the vector and return the result."
[v n]
(vec (concat (subvec v 0 n) (subvec v (inc n)))))
(defn remove-last
"Remove the last element in the vector and return the result."
[v]
(subvec v 0 (dec (count v))))
(defn remove-last-two
"Remove the last two elements in the vector and return the result."
[v]
(subvec v 0 (- (count v) 2)))
(defn insert-at
"Return a copy of the vector with new-item inserted at the given n. If
n is less than zero, the new item will be inserted at the beginning of
the vector. If n is greater than the length of the vector, the new item
will be inserted at the end of the vector."
[v n new-item]
(cond (< n 0) (into [new-item] v)
(>= n (count v)) (conj v new-item)
:default (vec (concat (conj (subvec v 0 n) new-item) (subvec v n)))))
(defn replace-at
"Replace the current element in the vector at index with the new-element
and return it."
[v index new-element]
(insert-at (delete-at v index) index new-element))
;;------------------------------------------------------------------------------
;; Tree id manipulation functions.
(defn tree-id->tree-id-parts
"Split a DOM id string (as used in this program) into its parts and return
a vector of the parts"
[id]
(s/split id topic-separator))
(defn tree-id-parts->tree-id-string
"Return a string formed by interposing the topic-separator between the
elements of the input vector."
[v]
(str (s/join topic-separator v)))
(defn increment-leaf-index
"Given the tree id of a leaf node, return an id with the node index
incremented."
[tree-id]
(let [parts (tree-id->tree-id-parts tree-id)
index-in-vector (- (count parts) 2)
leaf-index (int (nth parts index-in-vector))
new-parts (replace-at parts index-in-vector (inc leaf-index))]
(tree-id-parts->tree-id-string new-parts)))
(defn change-tree-id-type
"Change the 'type' of a tree DOM element id to something else."
[id new-type]
(let [parts (tree-id->tree-id-parts id)
shortened (remove-last parts)]
(str (tree-id-parts->tree-id-string shortened) (str topic-separator new-type))))
(defn tree-id->nav-vector-and-index
"Parse the id into a navigation path vector to the parent of the node and an
index within the vector of children. Return a map containing the two pieces
of data. Basically, parse the id into a vector of information to navigate
to the parent (a la get-n) and the index of the child encoded in the id."
[tree-id]
(let [string-vec (tree-id->tree-id-parts tree-id)
idx (int (nth string-vec (- (count string-vec) 2)))
without-last-2 (remove-last-two string-vec)
without-first (delete-at without-last-2 0)
index-vector (mapv int without-first)
interposed (interpose :children index-vector)]
{:path-to-parent (vec interposed) :child-index idx}))
;;------------------------------------------------------------------------------
;; Functions to manipulate the tree and subtrees.
(defn add-child!
"Insert the given topic at the specified index in the parents vector of
children. No data is deleted."
[parent-topic-ratom index topic-to-add]
(swap! parent-topic-ratom insert-at index topic-to-add))
(defn graft-topic!
"Add a new topic at the specified location in the tree. The topic is inserted
into the tree. No data is removed. Any existing information after the graft
is pushed down in the tree."
[root-ratom id-of-desired-node topic-to-graft]
(let [path-and-index (tree-id->nav-vector-and-index id-of-desired-node)]
(add-child! (r/cursor root-ratom (:path-to-parent path-and-index))
(:child-index path-and-index) topic-to-graft)))
;;;-----------------------------------------------------------------------------
;;; Functions to handle keystroke events.
(defn handle-enter-key-down!
"Handle a key-down event for the Enter/Return key. Insert a new headline
in the tree and focus it, ready for editing."
[root-ratom span-id]
(let [id-of-new-child (increment-leaf-index span-id)]
(graft-topic! root-ratom id-of-new-child empty-test-topic)
(let [id-of-new-editor (change-tree-id-type id-of-new-child "editor")
id-of-new-label (change-tree-id-type id-of-new-child "label")]
(swap-display-properties id-of-new-label id-of-new-editor)
(.focus (get-element-by-id id-of-new-editor)))))
(defn handle-key-down
"Detect key-down events and dispatch them to the appropriate handlers."
[evt root-ratom span-id]
(when
(= (.-key evt) "Enter") (handle-enter-key-down! root-ratom span-id)))
;;;-----------------------------------------------------------------------------
;;; Functions to build the control.
(defn build-topic-span
"Build the textual part of a topic/headline."
[root-ratom topic-ratom span-id]
(let [label-id (change-tree-id-type span-id "label")
editor-id (change-tree-id-type span-id "editor")]
[:span
[:label {:id label-id
:style {:display :initial}
:onClick (fn [e]
(swap-display-properties label-id editor-id)
(.focus (get-element-by-id editor-id))
(.stopPropagation e))}
@topic-ratom]
[:input {:type "text"
:id editor-id
:style {:display :none}
:onKeyDown #(handle-key-down % root-ratom span-id)
:onFocus #(.stopPropagation %)
:onBlur #(swap-display-properties label-id editor-id)
:onChange #(reset! topic-ratom (event->target-value %))
:value @topic-ratom}]]))
(defn tree->hiccup
"Given a data structure containing a hierarchical tree of topics, generate
hiccup to represent that tree. Also generates a unique, structure-based
id that is included in the hiccup so that the correct element in the
application state can be located when its corresponding HTML element is
clicked."
([root-ratom]
(tree->hiccup root-ratom root-ratom "root"))
([root-ratom sub-tree-ratom path-so-far]
[:ul
(doall
(for
[index (range (count @sub-tree-ratom))]
(let [t (r/cursor sub-tree-ratom [index])
topic-ratom (r/cursor t [:topic])
id-prefix (str path-so-far topic-separator index)
topic-id (str id-prefix topic-separator "topic")
span-id (str id-prefix topic-separator "span")]
^{:key topic-id}
[:li {:id topic-id}
[:div (build-topic-span root-ratom topic-ratom span-id)]])))]))
(defn home
"Return a function to layout the home (only) page."
[app-state-atom]
(fn [app-state-ratom]
[:div (tree->hiccup (r/cursor app-state-ratom [:tree]))]))
(r/render-component [home global-state-with-hierarchy]
(get-element-by-id "app"))
(我认为其中一些与问题无关,例如tree id操纵函数。它们只是在这里使构建示例更容易。)
该控件使用vector
来包含同级,关于在向量的末尾插入新元素的某些事情似乎会导致渲染时间发生变化。
当用户选择了最后一项并单击Return
时,浏览器控制台中将出现一条错误消息,提示将空参数传递给get-element-by-id
。这是由键盘处理功能handle-enter-key-down!
触发的。
标题列表中的项目实际上是两个HTML元素:label
(在用户不编辑时显示)和文本input
(在编辑过程中显示)。创建新标题后,将调用swap-display-properties
函数以使编辑器可见,然后将其聚焦。
在同级矢量的末尾创建标题时,新label
和文本input
的DOM标识符不可用于切换两个元素的可见性。因此,有关get-element-by-id
的空参数的错误消息。
但是它在所有其他位置上都能正常工作。
我已经复制了这个
我可以通过将对swap-display-properties
的呼叫延迟25毫秒或更长时间来强制其工作。
;; Wait for rendering to catch up.
(js/setTimeout #(do (swap-display-properties id-of-new-label id-of-new-editor)
(.focus (get-element-by-id id-of-new-editor))) 25)
我假设我可以使用Reacts componentDidMount
方法做些什么,但是我不明白为什么只有在同级向量的末尾插入新标题时才会失败。
所以...
任何想法都会受到赞赏。
答案 0 :(得分:1)
我认为您已经将问题识别为在Reagent中添加新元素与在DOM中创建新元素之间的竞争条件(在其中get-element-by-id正在寻找它)。
最简单的答案(除了在每个地方增加25毫秒的睡眠之外)是使用事件循环库(例如重新框架)来安排“设置焦点”事件,该事件将在 next 传递中处理事件循环。
顺便说一句,我never use concat
或subvec
。用take
和drop
保持简单,并始终用(vec ...)
包装fn的输出,以将其强制输入到无任何偷偷摸摸/问题懒惰的纯矢量中。
答案 1 :(得分:1)
浏览器的重点难以维护且令人困惑。
这就是我按Enter键时发生的事情
keydown事件触发
您可以通过嫁接主题添加新主题!
您可以切换样式,以便显示输入内容和标签。 隐藏
您专注于列表中的下一个元素
然后,在完成按键事件后,试剂会重新投放
如果您要点击的元素不是 列表中的最后一个元素
如果您要击中的元素来自,则是列表中的最后一个元素
无法发生聚焦,因为没有具有该id的元素
因此,当试剂随新创建的元素一起重新投放时,没有元素在焦点上
浏览器正在做什么
您创建的新元素与旧的焦点元素位于同一位置,因此浏览器将焦点保持在该位置
您可以使用以下代码段对此进行测试,该代码段可在每次按下按键时切换两个输入。
尽管ID和组件不同,但是焦点仍然保持在原处,即使交换两个组件也是如此
(defn test-comp []
(r/with-let [*test? (r/atom true)]
[:div
(if @*test?
[:div
[:input
{:value "test"
:id "test"
:on-key-down #(swap! *test? not)}]
[:input
{:value "not test"
:id "not test"
:on-key-down #(swap! *test? not)}]]
[:div
[:input
{:value "not test"
:id "not test"
:on-key-down #(swap! *test? not)}]
[:input
{:value "test"
:id "test"
:on-key-down #(swap! *test? not)}]])]))
(注意:这将向您发出警告,提示您没有on-change处理程序,但这对本演示并不重要,只想指定该值,以便您可以看到两个输入交换位置,但重点仍然在同一地点)
关于如何解决此问题...
不要依靠等待周期或使用js超时来解决这个问题,那只会浪费宝贵的时间
我建议不要使用浏览器保持焦点
简单的答案是使什么索引集中在应用程序状态中,然后根据什么来决定是显示标签还是输入
然后将自动聚焦属性添加到输入,以便在渲染时将其聚焦
一些有关如何使用试剂的指针
在您的代码中,您使用()解析了试剂成分,但是您应该使用[]
这与试剂决定何时重新渲染组件的方式有关,但是由于您解析了整个树,因此每次更改被取消引用的原子时,它都会重新渲染整个树,而不仅仅是重新引用该原子的位置。 (通过在build-topic-span组件中的代码中添加println进行测试)
在form-2组件中定义光标(或使用with-let),每个组件只需要定义一次,因此无需在每个后续渲染中都重新定义它们(不确定是否会导致错误) ,但这是一个好习惯)
您也可以像get-in一样使用游标,所以代替
t (r/cursor sub-tree-ratom [index])
topic-ratom (r/cursor t [:topic])
你可以做
topic-ratom (r/cursor t [index :topic])
其他一些笔记
您正在执行的交换样式操作会令人困惑,如果您跟踪所关注的内容,则可以根据所关注的内容来呈现不同的组件,而无需在dom中同时包含标签和输入在同一时间。
传递一堆字符串ID非常令人困惑,尤其是在调用graft-topic时!您将字符串解构回路径。数据更容易使用,将路径保留在向量中,仅在需要时才将其设置为字符串
此示例在重构时考虑了这些事情
(ns test-reagent-vector.core
(:require [clojure.string :as s]
[reagent.core :as r]))
(def ^{:constant true} topic-separator \u02D1)
(def empty-test-topic {:topic "Empty Test Topic"})
(defonce global-state-with-hierarchy
(r/atom {:name "Global Application State, Inc."
:focused-index nil
:data {:one "one" :two 2 :three [3]}
:tree [{:topic "First Headline"}
{:topic "Middle Headline"}
{:topic "Last Headline"}]}))
(defn get-element-by-id
[id]
(.getElementById js/document id))
(defn event->target-element
[evt]
(.-target evt))
(defn event->target-value
[evt]
(.-value (event->target-element evt)))
(defn swap-style-property
"Swap the specified style settings for the two elements."
[first-id second-id property]
(let [style-declaration-of-first (.-style (get-element-by-id first-id))
style-declaration-of-second (.-style (get-element-by-id second-id))
value-of-first (.getPropertyValue style-declaration-of-first property)
value-of-second (.getPropertyValue style-declaration-of-second property)]
(.setProperty style-declaration-of-first property value-of-second)
(.setProperty style-declaration-of-second property value-of-first)))
(defn swap-display-properties
"Swap the display style properties for the two elements."
[first-id second-id]
(swap-style-property first-id second-id "display"))
;;------------------------------------------------------------------------------
;; Vector-related manipulations.
(defn delete-at
"Remove the nth element from the vector and return the result."
[v n]
(vec (concat (subvec v 0 n) (subvec v (inc n)))))
(defn remove-last
"Remove the last element in the vector and return the result."
[v]
(subvec v 0 (dec (count v))))
(defn remove-last-two
"Remove the last two elements in the vector and return the result."
[v]
(subvec v 0 (- (count v) 2)))
(defn insert-at
"Return a copy of the vector with new-item inserted at the given n. If
n is less than zero, the new item will be inserted at the beginning of
the vector. If n is greater than the length of the vector, the new item
will be inserted at the end of the vector."
[v n new-item]
(cond (< n 0) (into [new-item] v)
(>= n (count v)) (conj v new-item)
:default (vec (concat (conj (subvec v 0 n) new-item) (subvec v n)))))
(defn replace-at
"Replace the current element in the vector at index with the new-element
and return it."
[v index new-element]
(insert-at (delete-at v index) index new-element))
;;------------------------------------------------------------------------------
;; Tree id manipulation functions.
(defn tree-id->tree-id-parts
"Split a DOM id string (as used in this program) into its parts and return
a vector of the parts"
[id]
(s/split id topic-separator))
(defn tree-id-parts->tree-id-string
"Return a string formed by interposing the topic-separator between the
elements of the input vector."
[v]
(str (s/join topic-separator v)))
(defn increment-leaf-index
"Given the tree id of a leaf node, return an id with the node index
incremented."
[tree-id]
(let [parts (tree-id->tree-id-parts tree-id)
index-in-vector (- (count parts) 2)
leaf-index (int (nth parts index-in-vector))
new-parts (replace-at parts index-in-vector (inc leaf-index))]
(tree-id-parts->tree-id-string new-parts)))
(defn change-tree-id-type
"Change the 'type' of a tree DOM element id to something else."
[id new-type]
(let [parts (tree-id->tree-id-parts id)
shortened (remove-last parts)]
(str (tree-id-parts->tree-id-string shortened) (str topic-separator new-type))))
(defn tree-id->nav-vector-and-index
"Parse the id into a navigation path vector to the parent of the node and an
index within the vector of children. Return a map containing the two pieces
of data. Basically, parse the id into a vector of information to navigate
to the parent (a la get-n) and the index of the child encoded in the id."
[tree-id]
(let [string-vec (tree-id->tree-id-parts tree-id)
idx (int (nth string-vec (- (count string-vec) 2)))
without-last-2 (remove-last-two string-vec)
without-first (delete-at without-last-2 0)
index-vector (mapv int without-first)
interposed (interpose :children index-vector)]
{:path-to-parent (vec interposed) :child-index idx}))
;;------------------------------------------------------------------------------
;; Functions to manipulate the tree and subtrees.
(defn add-child!
"Insert the given topic at the specified index in the parents vector of
children. No data is deleted."
[parent-topic-ratom index topic-to-add]
(swap! parent-topic-ratom insert-at index topic-to-add))
(defn graft-topic!
"Add a new topic at the specified location in the tree. The topic is inserted
into the tree. No data is removed. Any existing information after the graft
is pushed down in the tree."
[root-ratom id-of-desired-node topic-to-graft]
(let [path-and-index (tree-id->nav-vector-and-index id-of-desired-node)]
(add-child! (r/cursor root-ratom (:path-to-parent path-and-index))
(:child-index path-and-index) topic-to-graft)))
;;;-----------------------------------------------------------------------------
;;; Functions to handle keystroke events.
(defn handle-enter-key-down!
"Handle a key-down event for the Enter/Return key. Insert a new headline
in the tree and focus it, ready for editing."
[app-state root-ratom index]
(add-child! root-ratom (inc index) empty-test-topic)
(swap! app-state update :focused-index inc)
)
(defn handle-key-down
"Detect key-down events and dispatch them to the appropriate handlers."
[evt app-state root-ratom index]
(when (= (.-key evt) "Enter")
(handle-enter-key-down! app-state root-ratom index)))
;;;-----------------------------------------------------------------------------
;;; Functions to build the control.
(defn build-topic-span
"Build the textual part of a topic/headline."
[root-ratom index]
(r/with-let [topic-ratom (r/cursor root-ratom [index :topic])
focused-index (r/cursor global-state-with-hierarchy [:focused-index])]
(if-not (= index @focused-index)
[:label
{:onClick #(reset! focused-index index)}
@topic-ratom]
[:input {:type "text"
:auto-focus true
:onKeyDown #(handle-key-down % global-state-with-hierarchy root-ratom index)
:onChange #(reset! topic-ratom (event->target-value %))
:on-blur #(when (= index @focused-index)
(reset! focused-index nil))
:value @topic-ratom}])))
(defn tree->hiccup
"Given a data structure containing a hierarchical tree of topics, generate
hiccup to represent that tree. Also generates a unique, structure-based
id that is included in the hiccup so that the correct element in the
application state can be located when its corresponding HTML element is
clicked."
([root-ratom]
[tree->hiccup root-ratom root-ratom "root"])
([root-ratom sub-tree-ratom path-so-far]
[:ul
(doall
(for [index (range (count @sub-tree-ratom))]
^{:key (str index)}
[:li
[:div
[build-topic-span root-ratom index]]]
))]))
(defn home
"Return a function to layout the home (only) page."
[app-state-ratom]
(r/with-let [tree-ratom (r/cursor app-state-ratom [:tree])]
[:div
[tree->hiccup tree-ratom]]))
(r/render
[home global-state-with-hierarchy]
(get-element-by-id "app"))
我只改变了家,树→打ic,建立主题范围并处理键盘按下。
将来
我写的示例假定这是一个平面列表,但似乎您打算将来将其作为嵌套列表,如果是这样,我建议您进行一些更改
为每个主题关联唯一的ID,并使用该ID来确定该元素是否在焦点
指定到树上该点为止的id的向量的路径
不要将键指定为索引的函数,如果元素切换树中另一个元素的位置会怎样?我们不想重新渲染它。基于唯一的ID
调查试剂轨迹!询问当前元素是否聚焦时,可减少重新渲染的功能
希望这会有所帮助
如果您对如何构建嵌套的交互式列表还有其他疑问,请随时与我联系:)
答案 2 :(得分:0)
在约书亚·布朗(Joshua Brown)和艾伦·汤普森(Alan Thompson)的回答之后,我再次回顾了Reagent中的API文档,以了解with-let
所做的事情。
然后我注意到after-render
,这正是我所需要的。要解决我的示例中的问题,请在after-render
中添加handle-enter-key-down!
,如下所示。
(defn handle-enter-key-down!
"Handle a key-down event for the Enter/Return key. Insert a new headline
in the tree and focus it, ready for editing."
[root-ratom span-id]
(let [id-of-new-child (increment-leaf-index span-id)]
(graft-topic! root-ratom id-of-new-child empty-test-topic)
(let [id-of-new-editor (change-tree-id-type id-of-new-child "editor")
id-of-new-label (change-tree-id-type id-of-new-child "label")]
(r/after-render
(fn []
(swap-display-properties id-of-new-label id-of-new-editor)
(.focus (get-element-by-id id-of-new-editor)))))))
由于在渲染之后存在新label
和文本input
的标识符,因此现在可以按预期交换它们的显示属性,并且可以集中显示新可见的input
。
我相信,当在向量的其他位置插入新标题时,这也可以解决以前存在(但没有表现出来)的潜在比赛条件。