cljfx / cljfx

Declarative, functional and extensible wrapper of JavaFX inspired by better parts of react and re-frame
MIT License
958 stars 48 forks source link

Simple example of using a editable tabe in cljfx #143

Closed lispmarc closed 3 years ago

lispmarc commented 3 years ago

I'm retunring to clojure after many years and looking for a non web based GUI and found cljfx (I have used seesaw before). I'm looking now for some sample code to work wit tables with editable cells. I looked at the example 'e32_editable_table_cell_with_datascript.clj' but found it difficult to understand because the use of 'datascript' en context in the code. Is there a more simple example of how to work with table, showing just the essence.

Thanks a lot in advance

Marc

dsbw commented 3 years ago

I was in your shoes a couple of months back—with a side of needing to be able to tab from column-to-column for easy data entry—and ended up with the code below. I've got everything worked out except how to define the table in such a way that it knows what the next column is.

I can create a table using:

(editable-table [["Column 1" :column-1-data-field] ["Column 2" :column-2-data-field]]
(get-in context [:cljfx.context/m :next-level-to-data :level-after-that-to-data :etc]))}

The get-in-context part expects to be pointed to a vector of maps (the records to be displayed) with fields of :column-1-data-field and :column-2-data-field. (Mine are coming from postgraphile.)

You'll notice it's not that much different from the example, just a little simpler.

;====table stuff...for handling all the weird and complicated issues that arise when doing a table control====
(defn focus-when-on-scene! [node]
  (if (some? (.getScene node))
    (.requestFocus node)
    (.addListener (.sceneProperty node)
                  (reify ChangeListener
                    (changed [this _ _ new-scene]
                      (when (some? new-scene)
                        (.removeListener (.sceneProperty node) this)
                        (.requestFocus node)))))))

(defn editable-cell [{:keys [fx/context id attr value-converter] :as e}]
  (let [edit (fx/sub-val context :edit)
        value (attr id)]
    (when (= edit [id attr])
    (if (= edit [id attr])
      {:fx/type    fx/ext-on-instance-lifecycle
       :on-created focus-when-on-scene!                     ;; using it with `ext-on-instance-lifecycle`
       :desc       {:fx/type        :text-field
                    :on-key-pressed {:event/type :edit-next-column :next-column :data}
                    :text-formatter {:fx/type          :text-formatter
                                     :value-converter  value-converter
                                     :value            value
                                     :on-value-changed {:event/type :cell-commit
                                                        :attr       attr
                                                        :id         id}}}}
      {:fx/type          :label
       :on-mouse-clicked {:event/type :grid-activate-edit :id id :attr attr}
       :text             (str value)})))

(defn make-attr-cell-factory [view attr value-converter]
  (fn [id]
    {:text    ""
     :graphic {:fx/type         view
               :id              id
               :attr            attr
               :value-converter value-converter}}))

(defn etc [header data-column]
  {:fx/type            :table-column
   :text               header
   :cell-value-factory identity
   :cell-factory       {:fx/cell-type :table-cell
                        :describe     (make-attr-cell-factory editable-cell data-column :default)}})

(defn editable-table [columns items]
  {:fx/type :table-view
   :columns (map #(apply etc %) columns)
   :items   items})

Perhaps this is of some use.

lispmarc commented 3 years ago

It certainly is, one question however, I assume in editable-table that items is a vector of rows to be displayed in the table. how is a column mapped on a column value of a row in the vector. I guess I don't fully understand how a collection of rows with column values is mapped on the columns in a table.

Thanks

dsbw commented 3 years ago

You are correct. The items are a vector of maps:

[{:first-name "John" :last-name "Smith" :age 24} {:first-name "Bob" :last-name "Roberts" :age 45}...]

The data is extracted here:

(defn editable-cell [{:keys [fx/context id attr value-converter] :as e}]
  (let [edit (fx/sub-val context :edit)
        value (attr id)] ;<----

So, if you define your table:

(editable-table [["first" :first-name] ["last" :last-name]["age" :age]...

When you get to that line in editable-cell, it's:

(:first-name {:first-name "John" :last-name "Smith" :age 24})

then

{:last-name {:first-name "John" :last-name "Smith" :age 24})

then

{:age {:first-name "John" :last-name "Smith" :age 24})

Why is it called id when it's the whole record? Because I pulled it out of e32:

(defn- editable-cell [{:keys [fx/context id attr value-converter]}]
  (let [edit (fx/sub-val context :edit)
        value (fx/sub-ctx context value-sub id attr)]

In that case, you're literally just passing in the ID and the attribute and the value-sub routine is doing the extraction:

(defn- value-sub [ctx id attr]
  (fx/sub-ctx ctx query-sub '[:find ?v . :in $ ?e ?a :where [?e ?a ?v]] id attr))

For me, simplifying this started with not caring where the data comes from. In most of the editable-table cases I have, it's a specific record already pulled from a DB, so my call looks like:

(editable-table [["first-name" :first-name] ["last-name" :last-name]]
(get-in context [:cljfx.context/m :selected-customer]))}

Hope that helps.

lispmarc commented 3 years ago

Thank you this really helps

Marc

vlaaad commented 3 years ago

@dsbw thanks for helping out!