etaf/examples/etaf-data-example.el
2026-08-31 15:18:26 +08:00

201 lines
7.5 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))
(etaf-define-component etaf-data-example-row (&key controller task)
"Render one retained TASK row from CONTROLLER."
:view
(flex :width '(718) :flex-flow '(row nowrap) :gap '(0 (10))
:padding '(1 (12)) :border "#6D8A73"
:bgcolor
(if (etaf-value
(etaf-data-selected-ref controller (plist-get task :id)))
"#DCEBDD" "#FFFDF8")
:role 'button :use (list (etaf-focusable))
:on-press
(let ((data-controller controller)
(identity (plist-get task :id)))
(lambda ()
(etaf-dispatch 'etaf-data-example-toggle
data-controller identity)))
(box :width '(36) :font-weight 'bold
:color
(if (etaf-value
(etaf-data-selected-ref controller (plist-get task :id)))
"#2F6B43" "#8D887F")
(text
(expr
(if (etaf-value
(etaf-data-selected-ref controller (plist-get task :id)))
"" ""))))
(box :flex-grow 1 :flex-shrink 1 :flex-basis '(390)
:min-width '(280)
(text (expr (plist-get task :title))))
(box :width '(96) :color "#66706A"
(text (expr (plist-get task :owner))))
(box :width '(84) :font-weight 'bold :text-align 'right
:color (if (eq (plist-get task :status) 'done)
"#2F6B43" "#9B4A34")
(text (expr (upcase (symbol-name (plist-get task :status))))))))
(etaf-define-component etaf-data-example-filter
(&key controller label query border background)
"Render one query filter for CONTROLLER."
:view
(box :padding '(1 (12)) :border border :bgcolor background
:font-weight 'bold :role 'button :use (list (etaf-focusable))
:on-press
(let ((data-controller controller) (next-query query))
(lambda ()
(etaf-data-example--filter data-controller next-query)))
(text (expr label))))
(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)))
(list :controller controller :next-id next-id))
:view
(column :width '(720) :color "#252A2E" :bgcolor "#F8F5EE"
(box :width '(720) :padding '(1 (18)) :border "#8F432F"
:bgcolor "#FFFDF8" :text-align 'center
(column
(text :color "#8F432F" :font-weight 'bold
"BEST PRACTICE / DATA OWNERSHIP")
(text :font-weight 'bold "Task controller")
(text :color "#66706A"
(expr
(format
"%d records · %d selected"
(etaf-value
(etaf-data-total (plist-get (etaf-state) :controller)))
(length
(etaf-value
(etaf-data-selection (plist-get (etaf-state) :controller)))))))))
(box :height 1)
(flex :width '(720) :flex-flow '(row wrap) :gap '(1 (10))
(etaf-data-example-filter
:ref 'data-filter-all
:controller (plist-get (etaf-state) :controller)
:label "ALL" :query nil :border "#4E7890" :background "#D9EAF2")
(etaf-data-example-filter
:ref 'data-filter-open
:controller (plist-get (etaf-state) :controller)
:label "OPEN" :query '(:status open)
:border "#C97252" :background "#F1D4C9")
(etaf-data-example-filter
:ref 'data-filter-done
:controller (plist-get (etaf-state) :controller)
:label "DONE" :query '(:status done)
:border "#6D8A73" :background "#DCEBDD")
(box :padding '(1 (12)) :border "#7A6B95" :bgcolor "#E7E2F1"
:font-weight 'bold :ref 'data-add :role 'button
:use (list (etaf-focusable))
:on-press
(let ((controller (plist-get (etaf-state) :controller))
(next-id (plist-get (etaf-state) :next-id)))
(lambda ()
(etaf-dispatch 'etaf-data-example-add controller next-id)))
(text "+ ADD TASK")))
(box :height 1)
(column :width '(720)
(box :if
(null
(etaf-value
(etaf-data-items (plist-get (etaf-state) :controller))))
:width '(720) :padding '(2 (16))
:border "#8D887F" :bgcolor "#EEEAE2" :text-align 'center
(text "No matching tasks."))
(etaf-data-example-row
:for (task
(etaf-value
(etaf-data-items (plist-get (etaf-state) :controller))))
:key (plist-get task :id)
:ref (intern (format "data-task-%d" (plist-get task :id)))
:controller (plist-get (etaf-state) :controller)
:task task))
(box :height 1)
(box :width '(720) :padding '(1 (16)) :border "#8D887F"
:color "#4D5651" :bgcolor "#EEEAE2"
(text "Owner rule: create in setup, mutate through Data, stop on unmount"))))
;;;###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