fix: make component catalog package reload-safe
This commit is contained in:
parent
4d0d63bbb3
commit
6a1d816292
@ -52,8 +52,7 @@
|
|||||||
:color (plist-get theme color-key)
|
:color (plist-get theme color-key)
|
||||||
:font-weight (when (memq variant '(strong heading)) 'bold))))
|
:font-weight (when (memq variant '(strong heading)) 'bold))))
|
||||||
|
|
||||||
;;;###autoload
|
(etaf-ui--define-component etaf-label (&key text variant)
|
||||||
(etaf-define-component etaf-label (&key text variant)
|
|
||||||
"Render TEXT as a semantic Label."
|
"Render TEXT as a semantic Label."
|
||||||
:view
|
:view
|
||||||
(text
|
(text
|
||||||
@ -99,8 +98,7 @@
|
|||||||
(funcall callback)))))
|
(funcall callback)))))
|
||||||
state))
|
state))
|
||||||
|
|
||||||
;;;###autoload
|
(etaf-ui--define-component etaf-button
|
||||||
(etaf-define-component etaf-button
|
|
||||||
(&key label on-press disabled ref class color bgcolor border padding
|
(&key label on-press disabled ref class color bgcolor border padding
|
||||||
font-weight tab-index aria-label use variant)
|
font-weight tab-index aria-label use variant)
|
||||||
"Render a standard pressable Button with retained callback identity.
|
"Render a standard pressable Button with retained callback identity.
|
||||||
@ -154,8 +152,7 @@ across parent Component rerenders."
|
|||||||
:bgcolor (plist-get theme bg)
|
:bgcolor (plist-get theme bg)
|
||||||
:border (etaf-ui--style-border (plist-get theme border-key)))))
|
:border (etaf-ui--style-border (plist-get theme border-key)))))
|
||||||
|
|
||||||
;;;###autoload
|
(etaf-ui--define-component etaf-checkbox
|
||||||
(etaf-define-component etaf-checkbox
|
|
||||||
(&key checked label on-change disabled)
|
(&key checked label on-change disabled)
|
||||||
"Render a controlled Checkbox whose next value is sent to ON-CHANGE."
|
"Render a controlled Checkbox whose next value is sent to ON-CHANGE."
|
||||||
:view
|
:view
|
||||||
@ -187,8 +184,7 @@ across parent Component rerenders."
|
|||||||
("&.enabled" :padding (0 1))
|
("&.enabled" :padding (0 1))
|
||||||
(".etaf-checkbox-mark" :font-weight bold :width 1)))
|
(".etaf-checkbox-mark" :font-weight bold :width 1)))
|
||||||
|
|
||||||
;;;###autoload
|
(etaf-ui--define-component etaf-panel (&key title variant)
|
||||||
(etaf-define-component etaf-panel (&key title variant)
|
|
||||||
"Render a titled Panel with named header and default slots."
|
"Render a titled Panel with named header and default slots."
|
||||||
:render
|
:render
|
||||||
(let ((theme (etaf-ui--style-tokens
|
(let ((theme (etaf-ui--style-tokens
|
||||||
@ -216,8 +212,7 @@ across parent Component rerenders."
|
|||||||
("&" :padding (1 2))
|
("&" :padding (1 2))
|
||||||
(".etaf-panel-title" :font-weight bold)))
|
(".etaf-panel-title" :font-weight bold)))
|
||||||
|
|
||||||
;;;###autoload
|
(etaf-ui--define-component etaf-number-input
|
||||||
(etaf-define-component etaf-number-input
|
|
||||||
(&key value label on-change disabled min max)
|
(&key value label on-change disabled min max)
|
||||||
"Render a controlled minibuffer-backed NumberInput using Button."
|
"Render a controlled minibuffer-backed NumberInput using Button."
|
||||||
:render
|
:render
|
||||||
|
|||||||
@ -240,7 +240,7 @@ LOADING-LABEL, ERROR-LABEL, and EMPTY-LABEL override the state text."
|
|||||||
(when (eq kind 'error) "etaf-data-grid-error")
|
(when (eq kind 'error) "etaf-data-grid-error")
|
||||||
(and theme (plist-get theme :ui-data-grid-error-fg)))))
|
(and theme (plist-get theme :ui-data-grid-error-fg)))))
|
||||||
|
|
||||||
(etaf-define-component etaf-ui--data-grid-body-item
|
(etaf-ui--define-component etaf-ui--data-grid-body-item
|
||||||
(&key controller entry columns row-ref on-row-press row-selected-p
|
(&key controller entry columns row-ref on-row-press row-selected-p
|
||||||
grid-state)
|
grid-state)
|
||||||
"Render one retained keyed DataGrid ENTRY with a cached row action."
|
"Render one retained keyed DataGrid ENTRY with a cached row action."
|
||||||
@ -252,8 +252,7 @@ LOADING-LABEL, ERROR-LABEL, and EMPTY-LABEL override the state text."
|
|||||||
(etaf-ui--style-tokens
|
(etaf-ui--style-tokens
|
||||||
:ui-table-border :ui-table-selected-fg :ui-table-selected-bg))))
|
:ui-table-border :ui-table-selected-fg :ui-table-selected-bg))))
|
||||||
|
|
||||||
;;;###autoload
|
(etaf-ui--define-component etaf-data-grid
|
||||||
(etaf-define-component etaf-data-grid
|
|
||||||
(&key controller columns row-key on-row-press row-ref row-selected-p
|
(&key controller columns row-key on-row-press row-ref row-selected-p
|
||||||
loading-label error-label empty-label)
|
loading-label error-label empty-label)
|
||||||
"Render DATA CONTROLLER state through the public Component DSL.
|
"Render DATA CONTROLLER state through the public Component DSL.
|
||||||
@ -297,8 +296,7 @@ actions; failed candidates cannot mutate committed handler inputs."
|
|||||||
:grid-state (etaf-state)))
|
:grid-state (etaf-state)))
|
||||||
(slot :name 'footer)))
|
(slot :name 'footer)))
|
||||||
|
|
||||||
;;;###autoload
|
(etaf-ui--define-component etaf-pagination
|
||||||
(etaf-define-component etaf-pagination
|
|
||||||
(&key controller previous-ref next-ref class color bgcolor border padding
|
(&key controller previous-ref next-ref class color bgcolor border padding
|
||||||
aria-label)
|
aria-label)
|
||||||
"Render a controlled pager for DATA CONTROLLER with retained controls."
|
"Render a controlled pager for DATA CONTROLLER with retained controls."
|
||||||
|
|||||||
@ -12,6 +12,29 @@
|
|||||||
(require 'cl-lib)
|
(require 'cl-lib)
|
||||||
(require 'etaf)
|
(require 'etaf)
|
||||||
|
|
||||||
|
(defvar etaf-ui--component-definition-signatures
|
||||||
|
(make-hash-table :test #'eq)
|
||||||
|
"Exact catalog definitions already installed in the current process.")
|
||||||
|
|
||||||
|
(defmacro etaf-ui--define-component (name arguments &rest clauses)
|
||||||
|
"Define catalog Component NAME once for exact ARGUMENTS and CLAUSES.
|
||||||
|
|
||||||
|
`package.el' may reload dependency files after byte compilation. Identical
|
||||||
|
catalog definitions are idempotent across that reload, while a changed
|
||||||
|
definition still reaches `etaf-define-component' and therefore requires the
|
||||||
|
public `etaf-component-redefine-run' boundary."
|
||||||
|
(declare (indent 2) (debug defun))
|
||||||
|
(let ((signature
|
||||||
|
(secure-hash 'sha256
|
||||||
|
(prin1-to-string (list name arguments clauses)))))
|
||||||
|
`(unless (equal
|
||||||
|
(gethash ',name etaf-ui--component-definition-signatures)
|
||||||
|
,signature)
|
||||||
|
(prog1
|
||||||
|
(etaf-define-component ,name ,arguments ,@clauses)
|
||||||
|
(puthash ',name ,signature
|
||||||
|
etaf-ui--component-definition-signatures)))))
|
||||||
|
|
||||||
(defconst etaf-ui--style-palette
|
(defconst etaf-ui--style-palette
|
||||||
'(:ui-fg "#252A2E"
|
'(:ui-fg "#252A2E"
|
||||||
:ui-bg "#FFFDF8"
|
:ui-bg "#FFFDF8"
|
||||||
|
|||||||
@ -202,7 +202,7 @@ ROW-REF, ON-ROW-PRESS, and ROW-SELECTED-P define optional interaction."
|
|||||||
(lambda () (funcall callback row-value))))
|
(lambda () (funcall callback row-value))))
|
||||||
(etaf-ui--table-row-children row columns))))
|
(etaf-ui--table-row-children row columns))))
|
||||||
|
|
||||||
(etaf-define-component etaf-ui--table-header (&key columns)
|
(etaf-ui--define-component etaf-ui--table-header (&key columns)
|
||||||
"Render one retained Table header."
|
"Render one retained Table header."
|
||||||
:render (etaf-ui--table-header columns)
|
:render (etaf-ui--table-header columns)
|
||||||
:styles
|
:styles
|
||||||
@ -210,7 +210,7 @@ ROW-REF, ON-ROW-PRESS, and ROW-SELECTED-P define optional interaction."
|
|||||||
(".etaf-table-header" :font-weight bold :padding (0 1))
|
(".etaf-table-header" :font-weight bold :padding (0 1))
|
||||||
(".etaf-table-header-cell" :font-weight bold)))
|
(".etaf-table-header-cell" :font-weight bold)))
|
||||||
|
|
||||||
(etaf-define-component etaf-ui--table-row
|
(etaf-ui--define-component etaf-ui--table-row
|
||||||
(&key row identity columns row-ref on-row-press row-selected-p)
|
(&key row identity columns row-ref on-row-press row-selected-p)
|
||||||
"Render one retained Table ROW with stable IDENTITY."
|
"Render one retained Table ROW with stable IDENTITY."
|
||||||
:render
|
:render
|
||||||
@ -220,8 +220,7 @@ ROW-REF, ON-ROW-PRESS, and ROW-SELECTED-P define optional interaction."
|
|||||||
(styles
|
(styles
|
||||||
(".etaf-table-row" :padding (0 1))))
|
(".etaf-table-row" :padding (0 1))))
|
||||||
|
|
||||||
;;;###autoload
|
(etaf-ui--define-component etaf-table
|
||||||
(etaf-define-component etaf-table
|
|
||||||
(&key columns rows row-key row-ref on-row-press row-selected-p)
|
(&key columns rows row-key row-ref on-row-press row-selected-p)
|
||||||
"Render ordinary ROWS as a presentational Table.
|
"Render ordinary ROWS as a presentational Table.
|
||||||
|
|
||||||
|
|||||||
@ -106,7 +106,9 @@ it is not a claim that every attribute is a declared business prop.")
|
|||||||
"\\`etaf-ui-.*\\.el\\'"))
|
"\\`etaf-ui-.*\\.el\\'"))
|
||||||
(dolist (entry (etaf-ui-m0a--read-top-level-forms file))
|
(dolist (entry (etaf-ui-m0a--read-top-level-forms file))
|
||||||
(let ((form (plist-get entry :form)))
|
(let ((form (plist-get entry :form)))
|
||||||
(when (and (eq (car-safe form) 'etaf-define-component)
|
(when (and (memq (car-safe form)
|
||||||
|
'(etaf-define-component
|
||||||
|
etaf-ui--define-component))
|
||||||
(symbolp (cadr form))
|
(symbolp (cadr form))
|
||||||
(not (string-prefix-p "etaf-ui--"
|
(not (string-prefix-p "etaf-ui--"
|
||||||
(symbol-name (cadr form)))))
|
(symbol-name (cadr form)))))
|
||||||
|
|||||||
@ -30,6 +30,24 @@ Use Chinese punctuation when CHINESE-P is non-nil."
|
|||||||
(plist-get component :observed-business-props)
|
(plist-get component :observed-business-props)
|
||||||
(if chinese-p "、" ", "))))
|
(if chinese-p "、" ", "))))
|
||||||
|
|
||||||
|
(ert-deftest etaf-ui-package-autoloads-never-register-components ()
|
||||||
|
"Keep package activation from executing Component registration forms."
|
||||||
|
(dolist (file (directory-files default-directory t "\\.el\\'"))
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert-file-contents file)
|
||||||
|
(goto-char (point-min))
|
||||||
|
(while (re-search-forward "^;;;###autoload[[:space:]]*$" nil t)
|
||||||
|
(forward-comment (point-max))
|
||||||
|
(should-not (eq (car-safe (read (current-buffer)))
|
||||||
|
'etaf-define-component))))))
|
||||||
|
|
||||||
|
(ert-deftest etaf-ui-package-reload-keeps-identical-catalog-definitions ()
|
||||||
|
"Allow package.el to reload exact catalog files without duplicate owners."
|
||||||
|
(let ((before (etaf-ui-m0a-component-inventory)))
|
||||||
|
(dolist (file '("etaf-ui-basic.el" "etaf-ui-table.el" "etaf-ui-data.el"))
|
||||||
|
(load-file (expand-file-name file default-directory)))
|
||||||
|
(should (equal before (etaf-ui-m0a-component-inventory)))))
|
||||||
|
|
||||||
(ert-deftest etaf-ui-m0b1-readmes-match-runtime-component-manifest ()
|
(ert-deftest etaf-ui-m0b1-readmes-match-runtime-component-manifest ()
|
||||||
"Keep the README contract aligned with the runtime-derived inventory."
|
"Keep the README contract aligned with the runtime-derived inventory."
|
||||||
(let* ((components (etaf-ui-m0a-component-inventory))
|
(let* ((components (etaf-ui-m0a-component-inventory))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user