refactor: use wcb v5 createElement-safe static props
This commit is contained in:
parent
9079ab0848
commit
cc44789829
5 changed files with 46 additions and 39 deletions
|
|
@ -24,7 +24,7 @@
|
||||||
"firebase": "^12.11.0",
|
"firebase": "^12.11.0",
|
||||||
"firebase-tools": "^15.22.4",
|
"firebase-tools": "^15.22.4",
|
||||||
"netlify-cli": "^26.1.0",
|
"netlify-cli": "^26.1.0",
|
||||||
"web-component-base": "^4.1.2"
|
"web-component-base": "^5.0.0"
|
||||||
},
|
},
|
||||||
"author": "Ayo Ayco"
|
"author": "Ayo Ayco"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,19 @@ import { DURATIONS } from './leaderboard-read.js'
|
||||||
* `<cozy-leaderboard>` — a custom element that lets a developer compose the
|
* `<cozy-leaderboard>` — a custom element that lets a developer compose the
|
||||||
* leaderboard UI declaratively in HTML instead of wiring it in JavaScript.
|
* leaderboard UI declaratively in HTML instead of wiring it in JavaScript.
|
||||||
*
|
*
|
||||||
* Built on `web-component-base` (WCB) the idiomatic way: the view is a pure
|
* Built on `web-component-base` (WCB) the idiomatic way: the observed
|
||||||
* `html` template over a precomputed view-state object, WCB's own
|
* attributes are declared as `static props` (typed defaults; the base derives
|
||||||
* `attributeChangedCallback` feeds observed attributes into `this.props` and
|
* observedAttributes and feeds values into the reactive `this.props`), the view
|
||||||
* the `onChanges` hook, and updates go through the base class's `render()`.
|
* is a pure `html` template over a precomputed view-state object, and change
|
||||||
* Data access and user-facing strings stay in {@link LeaderBoardService} /
|
* reactions arrive through the `onChanges` hook. Data access and user-facing
|
||||||
* LeaderBoardReader — the element only turns query results into templates.
|
* strings stay in {@link LeaderBoardService} / LeaderBoardReader — the element
|
||||||
|
* only turns query results into templates.
|
||||||
*
|
*
|
||||||
* `static props` is deliberately NOT used: WCB 4.1.2 writes each prop's
|
* (WCB ≥5 is required: v4 wrote each prop default onto the element as an
|
||||||
* default onto the element as an attribute inside the constructor, which the
|
* attribute inside the constructor, which the custom-elements spec forbids and
|
||||||
* custom-elements spec forbids during synchronous construction — it breaks
|
* which broke `document.createElement('cozy-leaderboard')`. v5 defers that
|
||||||
* `document.createElement('cozy-leaderboard')` entirely. Plain
|
* reflection to connect and never clobbers authored attributes, making
|
||||||
* `observedAttributes` + `onChanges` gives the same reactivity without that.
|
* `static props` safe here.)
|
||||||
*
|
*
|
||||||
* Composition lives in HTML attributes; the storage backend (adapter) is set
|
* Composition lives in HTML attributes; the storage backend (adapter) is set
|
||||||
* once in JS via configureLeaderboard(), because env-var config can't live in
|
* once in JS via configureLeaderboard(), because env-var config can't live in
|
||||||
|
|
@ -61,10 +62,6 @@ const prettyTime = ms => {
|
||||||
const FORMATTERS = { time: prettyTime }
|
const FORMATTERS = { time: prettyTime }
|
||||||
const resolveFormat = name => FORMATTERS[name]
|
const resolveFormat = name => FORMATTERS[name]
|
||||||
|
|
||||||
// WCB coerces an empty attribute value to a non-string (`'' -> true`), so
|
|
||||||
// observed values are read through this guard before use.
|
|
||||||
const str = value => (typeof value === 'string' ? value : '')
|
|
||||||
|
|
||||||
// Inline styles (as WCB `html` style objects) — the same visual output the
|
// Inline styles (as WCB `html` style objects) — the same visual output the
|
||||||
// LeaderBoardReader produces for the imperative JS composition path.
|
// LeaderBoardReader produces for the imperative JS composition path.
|
||||||
const STYLES = {
|
const STYLES = {
|
||||||
|
|
@ -97,8 +94,16 @@ const tabStyle = active => ({
|
||||||
|
|
||||||
export class CozyLeaderboard extends WebComponent {
|
export class CozyLeaderboard extends WebComponent {
|
||||||
|
|
||||||
static get observedAttributes() {
|
// Declared attributes (kebab-cased on the element: score-order). String
|
||||||
return ['category', 'title', 'duration', 'score-order', 'format']
|
// defaults keep `this.props.*` typed as strings, so an unset or emptied
|
||||||
|
// attribute reads as '' rather than a coerced boolean. The base class derives
|
||||||
|
// observedAttributes from these keys.
|
||||||
|
static props = {
|
||||||
|
category: '',
|
||||||
|
title: '',
|
||||||
|
duration: '',
|
||||||
|
scoreOrder: '',
|
||||||
|
format: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// View state the template renders from. Either { board: false } (not
|
// View state the template renders from. Either { board: false } (not
|
||||||
|
|
@ -125,17 +130,17 @@ export class CozyLeaderboard extends WebComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WCB change hook — `property` is the attribute (kebab-case) name. A title
|
* WCB change hook — `property` is the camelCase prop name (WCB ≥5). A title
|
||||||
* change needs no re-query: the heading reads `this.props.title`, so WCB's
|
* change needs no re-query: the heading reads `this.props.title`, so the base
|
||||||
* own render already updated it. score-order/format changes rebuild the
|
* class's own render already updated it. score-order/format changes rebuild
|
||||||
* service so the new config actually takes effect.
|
* the service so the new config actually takes effect.
|
||||||
*/
|
*/
|
||||||
onChanges({ property, currentValue }) {
|
onChanges({ property }) {
|
||||||
// Invalidate the cached service even while disconnected, so a score-order/
|
// Invalidate the cached service even while disconnected, so a scoreOrder/
|
||||||
// format change made on a detached element takes effect on re-connect.
|
// format change made on a detached element takes effect on re-connect.
|
||||||
if (property === 'score-order' || property === 'format') this._svc = null
|
if (property === 'scoreOrder' || property === 'format') this._svc = null
|
||||||
if (!this._connected || property === 'title') return
|
if (!this._connected || property === 'title') return
|
||||||
this._mount(property === 'duration' ? (str(currentValue) || undefined) : undefined)
|
this._mount(property === 'duration' ? (this.props.duration || undefined) : undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
get template() {
|
get template() {
|
||||||
|
|
@ -143,7 +148,7 @@ export class CozyLeaderboard extends WebComponent {
|
||||||
if (!view.board) return html`<em>Leaderboard not configured.</em>`
|
if (!view.board) return html`<em>Leaderboard not configured.</em>`
|
||||||
return html`
|
return html`
|
||||||
<div style=${STYLES.wrapper}>
|
<div style=${STYLES.wrapper}>
|
||||||
<h3 style=${STYLES.heading}>${str(this.props.title)}</h3>
|
<h3 style=${STYLES.heading}>${this.props.title}</h3>
|
||||||
<div style=${STYLES.tabBar}>
|
<div style=${STYLES.tabBar}>
|
||||||
${view.tabs.map(tab => html`
|
${view.tabs.map(tab => html`
|
||||||
<button
|
<button
|
||||||
|
|
@ -184,13 +189,13 @@ export class CozyLeaderboard extends WebComponent {
|
||||||
const adapter = this.adapter || sharedConfig.adapter
|
const adapter = this.adapter || sharedConfig.adapter
|
||||||
if (!adapter) return null
|
if (!adapter) return null
|
||||||
const formatScore = this.formatScore
|
const formatScore = this.formatScore
|
||||||
|| resolveFormat(str(this.props.format))
|
|| resolveFormat(this.props.format)
|
||||||
|| sharedConfig.formatScore
|
|| sharedConfig.formatScore
|
||||||
|| resolveFormat(sharedConfig.format)
|
|| resolveFormat(sharedConfig.format)
|
||||||
|| String
|
|| String
|
||||||
this._svc = new LeaderBoardService({
|
this._svc = new LeaderBoardService({
|
||||||
adapter,
|
adapter,
|
||||||
scoreOrder: str(this.props.scoreOrder) || sharedConfig.scoreOrder || 'asc',
|
scoreOrder: this.props.scoreOrder || sharedConfig.scoreOrder || 'asc',
|
||||||
formatScore,
|
formatScore,
|
||||||
qualifies: this.qualifies || sharedConfig.qualifies,
|
qualifies: this.qualifies || sharedConfig.qualifies,
|
||||||
// User-facing strings — pass through so apps localize without touching the package.
|
// User-facing strings — pass through so apps localize without touching the package.
|
||||||
|
|
@ -219,7 +224,7 @@ export class CozyLeaderboard extends WebComponent {
|
||||||
}
|
}
|
||||||
const duration = durationArg
|
const duration = durationArg
|
||||||
?? this._activeDuration
|
?? this._activeDuration
|
||||||
?? (str(this.props.duration) || 'today')
|
?? (this.props.duration || 'today')
|
||||||
this._activeDuration = duration
|
this._activeDuration = duration
|
||||||
this._load(service, duration)
|
this._load(service, duration)
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +251,7 @@ export class CozyLeaderboard extends WebComponent {
|
||||||
|
|
||||||
let list
|
let list
|
||||||
try {
|
try {
|
||||||
const rows = await reader.list(str(this.props.category), durationId)
|
const rows = await reader.list(this.props.category, durationId)
|
||||||
list = (rows && rows.length)
|
list = (rows && rows.length)
|
||||||
? {
|
? {
|
||||||
rows: rows.map((row, index) => ({
|
rows: rows.map((row, index) => ({
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
"./dist"
|
"./dist"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"web-component-base": "^4.1.2"
|
"web-component-base": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cozy-games/utils": "workspace:*"
|
"@cozy-games/utils": "workspace:*"
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ importers:
|
||||||
specifier: ^26.1.0
|
specifier: ^26.1.0
|
||||||
version: 26.1.0(@types/node@25.5.2)(picomatch@4.0.4)(supports-color@10.2.2)
|
version: 26.1.0(@types/node@25.5.2)(picomatch@4.0.4)(supports-color@10.2.2)
|
||||||
web-component-base:
|
web-component-base:
|
||||||
specifier: ^4.1.2
|
specifier: ^5.0.0
|
||||||
version: 4.1.2
|
version: 5.0.0
|
||||||
|
|
||||||
packages/leaderboard:
|
packages/leaderboard:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -78,8 +78,8 @@ importers:
|
||||||
specifier: ^12.11.0
|
specifier: ^12.11.0
|
||||||
version: 12.11.0
|
version: 12.11.0
|
||||||
web-component-base:
|
web-component-base:
|
||||||
specifier: ^4.1.2
|
specifier: ^5.0.0
|
||||||
version: 4.1.2
|
version: 5.0.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@cozy-games/utils':
|
'@cozy-games/utils':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
|
|
@ -6412,8 +6412,8 @@ packages:
|
||||||
wcwidth@1.0.1:
|
wcwidth@1.0.1:
|
||||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
||||||
|
|
||||||
web-component-base@4.1.2:
|
web-component-base@5.0.0:
|
||||||
resolution: {integrity: sha512-Jti4FHgCcwtsFWJ+PPwFNhTm5AJVIHrTXDew0rk2Y3b8EChRIE5xr6fmUni43tOhc6w8HatvR3k+qnxjXr/7Mw==}
|
resolution: {integrity: sha512-93W5bhqy0YkyOFI+4V6U7BNVbLR242CyGDKLuNIYekq2J4P4kg3qcWPbtQDNpa7LvZz/M8p86PzI+Mmq5/fvkw==}
|
||||||
|
|
||||||
web-streams-polyfill@3.3.3:
|
web-streams-polyfill@3.3.3:
|
||||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||||
|
|
@ -13869,7 +13869,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
defaults: 1.0.4
|
defaults: 1.0.4
|
||||||
|
|
||||||
web-component-base@4.1.2: {}
|
web-component-base@5.0.0: {}
|
||||||
|
|
||||||
web-streams-polyfill@3.3.3: {}
|
web-streams-polyfill@3.3.3: {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,5 @@ allowBuilds:
|
||||||
sharp: false
|
sharp: false
|
||||||
unix-dgram: false
|
unix-dgram: false
|
||||||
web-component-base: false
|
web-component-base: false
|
||||||
|
minimumReleaseAgeExclude:
|
||||||
|
- web-component-base@5.0.0
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue