Реагент не отображается должным образом при добавлении нового элемента в конце реактивного вектора - PullRequest
0 голосов
/ 25 февраля 2019

Я работаю над управлением деревом в ClojureScript и Reagent.Его можно использовать как навигатор файловой системы, навигатор тем, планировщик, и т. Д. .

Когда заголовок в схеме выбран и редактируется, традиционное поведение при нажатии Returnсоздать новый заголовок (дочерний или родной в зависимости от состояния расширения заголовка и того, есть ли у него дочерние элементы или нет), а затем сфокусировать его, оставив его готовым для редактирования.Мой элемент управления делает все это правильно, за исключением случая редактирования последнего родственника в группе.

В проблемном случае заголовок создается, как и ожидалось, но фокусировка на новом элементе управления завершается неудачно.

Я создал MCVE , используя figwheel шаблон .

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"))

(Я полагаю, что некоторые из них не имеют отношения к проблеме, например, функции манипуляции с идентификатором дерева. Они просто здесь, чтобы упростить построение примера.)

Элемент управленияиспользует vector, чтобы содержать братьев и сестер, что-то из-за вставки нового элемента в конце вектора, похоже, вызывает изменение времени рендеринга.

Когда пользователь выбрал последний элемент и нажал Returnв консоли браузера появляется сообщение об ошибке, в которой нулевой аргумент передается get-element-by-id.Это инициируется функцией обработки клавиатуры handle-enter-key-down!.

Элементы в списке заголовков на самом деле представляют собой два элемента HTML: label, который отображается, когда пользователь не редактирует его, и текст input это отображается во время редактирования.Когда создается новый заголовок, вызывается функция swap-display-properties, чтобы сделать редактор видимым, затем он фокусируется.

Когда заголовок создается в конце вектора братьев и сестер, идентификаторы DOM дляновые label и текст input недоступны для переключения видимости двух элементов.Таким образом, сообщение об ошибке с нулевым аргументом для get-element-by-id.

Но оно работает правильно для всех остальных позиций.

Я воспроизвел это

  • на Mac
  • с OpenJDK 9 и 11
  • с исходными версиями зависимостей, использованных в шаблоне, и после обновления их до текущих
  • в Safari и Firefox

Я могу заставить заставить его работать, отложив вызов до 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 метод, но я не понимаю, почему происходит сбой только при вставке нового заголовка в конце вектора братьев и сестер.

Итак ...

  • Этопросто совпадение, что другие случаи работают как положено?
  • Неужели я что-то неправильно понимаю в процессе работы с реагентами?
  • Есть ли проблемы с реагентом?

Есть идеи?был бы признателен.

Ответы [ 3 ]

0 голосов
/ 25 февраля 2019

Фокус в браузере сложно и запутанно поддерживать.

Вот то, что я думаю, что происходит, когда вы нажимаете Enter

Событие нажатия клавиш запускается

  • Вы добавляете новую тему через graft-topic!

  • Вы переключаете стили так, что ввод отображается, а метка скрыта

  • Высфокусируйтесь на следующем элементе в списке

, затем, после того, как событие keydown выполнено, реагенты повторно отображают

  • Когда это происходит, элемент, который сфокусирован, заменяетсяновым элементом, который вы создали

В случае, если элемент, по которому вы нажимаете, вводится из , а не , последний элемент в списке

  • Вымогут сфокусироваться на уже существующем элементе, а затем реактив заменяет этот элемент новым элементом, созданным в graft-topic!

В случае, если элемент, по которому вы нажимаете, введите из значение последний элемент в списке

  • Не удается выполнить фокусировку, поскольку элемент с таким идентификатором еще не существует

  • , поэтому ни один элемент не находится в фокусе, когда реагент повторно обращается к вновь созданному элементу

Что делает браузер

Новый созданный вами элемент находится в том же месте, что и старый сфокусированный элемент, поэтому браузер сохраняет фокус в этом месте

Вы можете проверить это с помощью следующего фрагмента кода, который переключает два входа при каждом нажатии клавиши.

Несмотря на то, что разные идентификаторы и разные компоненты, фокус остается на том же месте, что и при замене двух.компоненты

(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)}]])]))

(примечание: это даст вам предупреждение об отсутствии обработчика при изменении, но это не важно для этой демонстрации, просто хотел указать значение, чтобы вы могли видеть своп двух входовместа, но фокус остается на том же месте)

Что касается того, как это исправить ...

Не полагайтесь на ожидание цикла или использованиеJS тайм-аут, чтобы исправить это, это просто тратит драгоценное время

Я бы порекомендовал не использовать браузер для сохранения фокуса

Простой ответ - сохранить какой индекс находится в состоянии приложения, а затемрешить, будет ли отображаться метка или вход на основе того, что

Затем добавьте атрибут автофокуса к входу, чтобы при рендеринге он попадал в фокус

Некоторые указатели длякак использовать реагент

В вашем коде вы разрешаете компонент реагента с помощью (), но вам следует использовать []

Это связано с тем, как реагент решает, когда следует выполнить повторную визуализацию.компоненты, но так как вы разрешили все дерево, каждый раз, когда вы меняете атом, который вы переопределили, он будет перерисовывать все ваше дерево, а не только место, где вы переопределили атом.(проверьте это, добавив println в ваш код в компоненте build-topic-span)

Определите курсоры в компоненте формы-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.

передача нескольких строковых идентификаторов очень сбивает с толкуособенно при вызове привитой темы!вы деструктурируете строку обратно на путь.С данными гораздо проще работать, сохраняйте путь в векторе и делайте его строкой только тогда, когда он должен быть

Этот пример с учетом этих факторов

(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"))

Я толькоизменил дом, дерево → сбой, создайте диапазон темы и обработайте нажатие клавиши.

В будущем

В примере, который я написал, предполагается, что это плоский список, но кажется, что вы планируете сделать его вложенным списком в будущем, и если это правда, я бы порекомендовал изменить некоторые вещи

, связать уникальный идентификаторк каждой теме и используйте этот идентификатор, чтобы определить, находится ли этот элемент в фокусе.

указать путь до того момента, пока вектор идентификаторов до этой точки в дереве

неукажите ключ как функцию индекса, что если элемент поменяется местами с другим элементом в дереве?мы не хотим перерисовывать это.На основании уникального идентификатора

исследуйте трек с реагентами!функция для сокращения повторений при запросе, фокусируется ли текущий элемент

Надеюсь, это поможет

Не стесняйтесь сообщать мне, если у вас есть еще вопросы относительно того, как создать вложенный интерактивный список :)

0 голосов
/ 26 февраля 2019

После ответов Джошуа Брауна и Алана Томпсона я снова пересмотрел документацию по API в Reagent, чтобы понять, что сделал 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 может быть сфокусирован.

Я полагаю, это также исправляет потенциальное состояние гонки, которое существовало ранее (но не проявлялось) при вставке новых заголовков в другие позиции ввектор.

0 голосов
/ 25 февраля 2019

Я думаю, что вы уже определили проблему как условие гонки между добавлением нового элемента в Reagent и его созданием в DOM (где его ищет get-element-by-id).

Самый простой ответ (помимо добавления 25 мс спящих повсюду) - использовать библиотеку цикла событий, такую ​​как re-frame, для планирования события «set-focus», которое будет обрабатываться при следующем прохождении через цикл событий.


В качестве отступления я никогда не использую concat или subvec.Упростите это с помощью take & drop и всегда оборачивайте вывод fn с помощью (vec ...), чтобы принудительно преобразовать его в простой вектор без всякой подлости / проблемной лени.

...