我正在研究ClojureScript和Reagent中的树控件。它可以用作文件系统导航器、主题导航器、输出程序等。
当一个大纲中的标题被选中并被编辑时,当点击Return时,传统的行为是创建一个新的标题(根据标题的扩展状态以及它是否已经有子)创建一个新的标题(子标题或兄弟标题,以及它是否已经有子标题),然后将其聚焦,让它随时准备编辑。除了编辑组中的最后一个同级之外,我的控件都正确地做到了这一点。
在有问题的情况下,标题是按预期创建的,但是新控件的聚焦失败了。
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"))(我假设其中一些与问题无关,比如树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)我想我可以用React componentDidMount方法做一些事情,但我不明白为什么只有在兄弟姐妹向量的末尾插入一个新标题时才会失败。
所以..。
任何想法都将不胜感激。
发布于 2019-02-25 05:28:12
我认为您已经将问题确定为在Reagent中添加新元素和在DOM中创建它之间的争用条件(在DOM中,get- element id正在寻找它)。
最简单的答案(除了增加25 ms睡眠到处)是使用事件循环库,如重新帧,以安排一个“设置焦点”事件,将在下一次通过事件循环处理。
顺便说一句,I concat或subvec。在take & drop中保持简单,并且总是用(vec ...)包装fn的输出,以强制将它放到普通向量w/o中--任何偷偷摸摸的/有问题的懒惰。
发布于 2019-02-25 09:17:52
浏览器焦点很难维护,而且令人困惑。
以下是我认为当你按回车时发生的事情
按键事件触发
然后,在完成按键事件后,试剂重选器
如果您从点击enter的元素不是,则列表中的最后一个元素
如果您从点击enter的元素是,则列表中的最后一个元素
浏览器正在做什么
您创建的新元素与旧的焦点元素位于同一个位置,因此浏览器将焦点保留在该位置。
您可以使用下面的代码片段对此进行测试,该代码段将在每次按键上切换两个输入。
尽管有不同的ids和不同的组件,焦点仍然保持在相同的位置,甚至在交换这两个组件时也是如此。
(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超时来修复这个问题,这只是浪费宝贵的时间。
我建议不要使用浏览器来保留焦点。
简单的答案是保持什么索引集中在app状态,然后根据什么来决定标签还是输入。
然后将一个自动焦点属性添加到输入中,这样当它呈现时,它就会进入焦点。
关于如何使用试剂的一些提示
在您的代码中,您正在使用()解析试剂组件,但是您应该使用[]
这与试剂如何决定什么时候重装组件有关,但是既然你解决了整棵树,每次你改变一个原子时,它就会使你的整棵树重新排列,而不仅仅是你去除原子的地方。(通过在构建主题-span组件中向代码中添加println来测试这一点)
在form-2组件中定义游标(或使用-let),每个组件只需要定义一次游标,因此无需在每次后续呈现时重新定义游标(不确定这是否会导致错误,但这是一个良好的实践)
此外,您也可以使用游标,如get,所以,而不是
t (r/cursor sub-tree-ratom [index])
topic-ratom (r/cursor t [:topic])你可以做到
topic-ratom (r/cursor t [index :topic]),其他一些笔记,
您所做的交换样式是令人困惑的,如果您跟踪所关注的是什么,您只需根据所关注的内容呈现一个不同的组件,而不需要同时在dom中同时使用标签和输入。
传递一串字符串is是非常令人困惑的,特别是当调用嫁接-主题!将字符串重新构造回路径中。数据处理起来容易得多,将路径保持在向量中,并且只有在需要时才使其成为字符串。
这个例子考虑到了这些问题。
(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"))我只改变了家,树→打嗝,构建主题跨度和处理按键。
在未来的
我编写的示例假设这是一个平面列表,但似乎您计划在将来将其作为嵌套列表,如果是这样的话,我建议您更改一些内容
将唯一的id关联到每个主题,并使用该id来确定该元素是否处于焦点。
将路径指定为到树中该点为止的in的向量。
不要将键指定为索引的函数,如果元素切换到树中的另一个元素,怎么办?我们不想重蹈覆辙。以唯一的id为基础
调查试剂追踪!函数在询问当前元素是否有焦点时,可以减少重发器的数量。
希望这能有所帮助
如果您还有其他关于如何构建嵌套交互列表的问题,请随时留言给我:)
发布于 2019-02-25 21:48:27
在约书亚·布朗和艾伦·汤普森的回复之后,我再次回顾了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可以聚焦。
我相信这也修正了以前在载体的其他位置插入新标题时存在的潜在种族条件(但没有表现出来)。
https://stackoverflow.com/questions/54857329
复制相似问题