etaf/examples/etaf-data-example.el

191 lines
7.7 KiB
EmacsLisp

;;; etaf-data-example.el --- Data Controller example -*- lexical-binding: t; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; This example owns a Data Controller at the Component boundary, stops it on
;; unmount, and keeps query, selection, and mutation on public Data APIs.
;;; Code:
(require 'etaf)
(defconst etaf-data-example-buffer-name "*ETAF Data Example*"
"Buffer opened by `etaf-data-example-open'.")
(defconst etaf-data-example-tasks
'((:id 1 :title "Define the public boundary" :owner "Ada" :status open)
(:id 2 :title "Verify retained updates" :owner "Grace" :status done)
(:id 3 :title "Document cleanup ownership" :owner "Alan" :status open)
(:id 4 :title "Review visual hierarchy" :owner "Barbara" :status open)
(:id 5 :title "Run the complete gate" :owner "Edsger" :status done))
"Initial records used by the Data Controller example.")
(etaf-action-define etaf-data-example-add (runtime controller next-id)
"Insert one task into CONTROLLER using NEXT-ID inside RUNTIME."
(ignore runtime)
(let ((identity (etaf-value next-id)))
(etaf-data-mutate
controller 'insert
(list :id identity
:title (format "Review example #%d" identity)
:owner "Team"
:status 'open))
(etaf-set-value next-id (1+ identity))))
(etaf-action-define etaf-data-example-toggle (runtime controller identity)
"Toggle IDENTITY selection in CONTROLLER inside RUNTIME."
(ignore runtime)
(etaf-data-select controller identity
(not (etaf-data-selected-p controller identity))))
(defun etaf-data-example--filter (controller query)
"Set CONTROLLER to QUERY and load its matching records."
(etaf-data-set-query controller query)
(etaf-data-load controller))
(defun etaf-data-example--row (controller task)
"Return one TASK row bound to CONTROLLER."
(let* ((identity (plist-get task :id))
(selected (etaf-data-selected-p controller identity))
(status (plist-get task :status))
(host-ref (intern (format "data-task-%d" identity))))
(etaf-view
(box :layout 'flex :width '(718) :flex-flow '(row nowrap) :gap '(0 (10))
:padding '(1 (12)) :border "#6D8A73"
:bgcolor (if selected "#DCEBDD" "#FFFDF8")
:ref host-ref :role 'button :use (list (etaf-focusable))
:on-press (lambda ()
(etaf-dispatch 'etaf-data-example-toggle
controller identity))
(text :width '(36) :face 'bold
:color (if selected "#2F6B43" "#8D887F")
(expr :value (if selected "" "")))
(text :flex-grow 1 :flex-shrink 1 :flex-basis '(390)
:min-width '(280)
(expr :value (plist-get task :title)))
(text :width '(96) :color "#66706A"
(expr :value (plist-get task :owner)))
(text :width '(84) :face 'bold :text-align 'right
:color (if (eq status 'done) "#2F6B43" "#9B4A34")
(expr :value (upcase (symbol-name status))))))))
(defun etaf-data-example--header (controller)
"Return the summary header for CONTROLLER."
(let ((total (etaf-value (etaf-data-total controller)))
(selection (etaf-value (etaf-data-selection controller))))
(etaf-view
(box :layout 'column :width '(720) :padding '(1 (18)) :border "#8F432F"
:bgcolor "#FFFDF8" :text-align 'center
(text :color "#8F432F" :face 'bold
"BEST PRACTICE / DATA OWNERSHIP")
(text :face 'bold "Task controller")
(text :color "#66706A"
(expr :value
(format "%d records · %d selected" total
(length selection))))))))
(defun etaf-data-example--filter-control
(controller label host-ref query border background)
"Return a filter LABEL for CONTROLLER using HOST-REF and QUERY.
Use BORDER and BACKGROUND for its semantic color family."
(etaf-view
(text :padding '(1 (12)) :border border :bgcolor background
:face 'bold :ref host-ref :role 'button
:use (list (etaf-focusable))
:on-press (lambda ()
(etaf-data-example--filter controller query))
(expr :value label))))
(defun etaf-data-example--toolbar (controller next-id)
"Return the action toolbar for CONTROLLER and NEXT-ID."
(etaf-view
(box :layout 'flex :width '(720) :flex-flow '(row wrap) :gap '(1 (10))
(expr :value (etaf-data-example--filter-control
controller "ALL" 'data-filter-all nil
"#4E7890" "#D9EAF2"))
(expr :value (etaf-data-example--filter-control
controller "OPEN" 'data-filter-open '(:status open)
"#C97252" "#F1D4C9"))
(expr :value (etaf-data-example--filter-control
controller "DONE" 'data-filter-done '(:status done)
"#6D8A73" "#DCEBDD"))
(text :padding '(1 (12)) :border "#7A6B95" :bgcolor "#E7E2F1"
:face 'bold :ref 'data-add :role 'button
:use (list (etaf-focusable))
:on-press (lambda ()
(etaf-dispatch 'etaf-data-example-add
controller next-id))
"+ ADD TASK"))))
(defun etaf-data-example--rows (controller)
"Return the loaded task rows for CONTROLLER."
(let ((items (etaf-value (etaf-data-items controller))))
(if items
(mapcar (lambda (task)
(etaf-data-example--row controller task))
items)
(etaf-view
(text :width '(720) :padding '(2 (16))
:border "#8D887F" :bgcolor "#EEEAE2"
:text-align 'center "No matching tasks.")))))
(defun etaf-data-example--view (controller next-id)
"Return the Data example View for CONTROLLER and NEXT-ID."
(etaf-view
(box :layout 'column :width '(720) :color "#252A2E" :bgcolor "#F8F5EE"
(expr :value (etaf-data-example--header controller))
(box :height 1)
(expr :value (etaf-data-example--toolbar controller next-id))
(box :height 1)
(box :layout 'column :width '(720)
(expr :value (etaf-data-example--rows controller)))
(box :height 1)
(text :width '(720) :padding '(1 (16)) :border "#8D887F"
:color "#4D5651" :bgcolor "#EEEAE2"
"Owner rule: create in setup, mutate through Data, stop on unmount"))))
(etaf-define-component etaf-data-example-app ()
"Render a memory-backed task application with owned cleanup."
:setup
(let* ((source (etaf-data-memory-source
etaf-data-example-tasks :id-key :id
:name 'etaf-data-example))
(controller (etaf-data-controller
source :page-size 20
:name 'etaf-data-example))
(next-id (etaf-ref 6 :name 'etaf-data-example-next-id)))
(etaf-on-mounted (lambda () (etaf-data-load controller)))
(etaf-on-unmounted (lambda () (etaf-data-stop controller)))
(lambda () (etaf-data-example--view controller next-id))))
;;;###autoload
(defun etaf-data-example-view ()
"Return the Data Controller example View."
(etaf-view (etaf-data-example-app)))
;;;###autoload
(defun etaf-data-example-open ()
"Mount the Data Controller example and return its buffer."
(interactive)
(let ((buffer (etaf-mount etaf-data-example-buffer-name
(etaf-data-example-view))))
(when (called-interactively-p 'interactive)
(pop-to-buffer buffer))
buffer))
;;;###autoload
(defun etaf-data-example-close ()
"Unmount and kill the Data Controller example buffer."
(interactive)
(when-let* ((runtime
(etaf-runtime-for-buffer etaf-data-example-buffer-name)))
(etaf-unmount runtime))
(when-let* ((buffer (get-buffer etaf-data-example-buffer-name)))
(kill-buffer buffer)))
(provide 'etaf-data-example)
;;; etaf-data-example.el ends here