Skip to content

Commit 9394d75

Browse files
committed
feat: added usePropChanged hook
1 parent 0a24227 commit 9394d75

File tree

5 files changed

+79
-19
lines changed

5 files changed

+79
-19
lines changed

src/utils/base-element.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import { LitElement } from "lit";
1+
import { LitElement, PropertyValues } from "lit";
2+
3+
export type ObservedAttribute = { dependencies: string[], callback: Function };
24

35
export default abstract class BaseElement extends LitElement {
46
mounted: boolean = false;
7+
observedProperties: Map<string, ObservedAttribute> = new Map();
8+
updatedCallback: LitElement['updated'] | null = null;
59

610
onUnMount() {}
711

@@ -11,8 +15,32 @@ export default abstract class BaseElement extends LitElement {
1115
this.onUnMount();
1216
}
1317

18+
updated(changedProperties: PropertyValues) {
19+
super.updated(changedProperties);
20+
21+
this.observedProperties.forEach(
22+
({dependencies, callback}) => {
23+
if (dependencies.every( dep => changedProperties.has(dep) && changedProperties.get(dep) !== undefined )) {
24+
callback();
25+
}
26+
}
27+
)
28+
29+
if (this.updatedCallback) {
30+
this.updatedCallback(changedProperties);
31+
}
32+
}
33+
34+
usePropChanged(callback: Function, dependencies: string[]) {
35+
const cacheKey = dependencies.join('-');
36+
37+
// we should set it every time
38+
// otherwise the callback's closure is gonna be a snapshot
39+
this.observedProperties.set(cacheKey, {dependencies, callback});
40+
}
41+
1442
setUpdatedHook(fn: LitElement['updated']) {
15-
this.updated = fn.bind(this);
43+
this.updatedCallback = fn.bind(this);
1644
}
1745

1846
setAttrChangedHook(fn: LitElement['attributeChangedCallback']) {

src/utils/component.spec.ts

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, describe, it, vi } from 'vitest';
1+
import { expect, describe, it, vi, afterEach } from 'vitest';
22
import { elementUpdated, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
33
import component, { Props } from '../';
44
import { PropertyValues } from 'lit';
@@ -23,34 +23,56 @@ describe('component', () => {
2323
});
2424

2525
describe('properties', () => {
26-
function MyTestComponent2({useProp}: Props) {
26+
const usePropChangedCallback = vi.fn();
27+
28+
function MyTestComponent2({useProp, usePropChanged}: Props) {
2729
const [counter, setCounter] = useProp('counter', {type: Number}, 12);
2830

31+
usePropChanged(usePropChangedCallback, ['counter']);
32+
2933
return html`<div>
3034
<span class="counter">${counter}</span>
3135
<button @click=${() => setCounter(counter + 1)}>increase</button>
3236
</div>
3337
`;
3438
}
3539

36-
component(MyTestComponent2);
40+
afterEach(() => {
41+
usePropChangedCallback.mockReset();
42+
});
3743

38-
it('should define counter property with a default value', async () => {
39-
const element = await fixture(html`<my-test-component2></my-test-component2>`);
44+
component(MyTestComponent2);
4045

41-
const counter = element.shadowRoot?.querySelector('.counter');
42-
expect(counter?.textContent).toBe('12');
46+
describe('useProp', () => {
47+
it('should define counter property with a default value', async () => {
48+
const element = await fixture(html`<my-test-component2></my-test-component2>`);
49+
50+
const counter = element.shadowRoot?.querySelector('.counter');
51+
expect(counter?.textContent).toBe('12');
52+
});
53+
54+
it('should increase the value of the counter property using the setter', async () => {
55+
const element = await fixture(html`<my-test-component2></my-test-component2>`);
56+
const btn = element.shadowRoot?.querySelector('button');
57+
58+
btn?.click();
59+
await elementUpdated(element);
60+
61+
const counter = element.shadowRoot?.querySelector('.counter');
62+
expect(counter?.textContent).toBe('13');
63+
});
4364
});
4465

45-
it('should increase the value of the counter property using the setter', async () => {
46-
const element = await fixture(html`<my-test-component2></my-test-component2>`);
47-
const btn = element.shadowRoot?.querySelector('button');
48-
49-
btn?.click();
50-
await elementUpdated(element);
66+
describe('usePropChanged', () => {
67+
it('should trigger a callback on "counter" change', async () => {
68+
const element = await fixture(html`<my-test-component2></my-test-component2>`);
69+
const btn = element.shadowRoot?.querySelector('button');
70+
71+
btn?.click();
72+
await elementUpdated(element);
5173

52-
const counter = element.shadowRoot?.querySelector('.counter');
53-
expect(counter?.textContent).toBe('13');
74+
expect(usePropChangedCallback).toHaveBeenCalledOnce();
75+
});
5476
});
5577
});
5678

src/utils/hooks.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,8 @@ export class Hooks {
7171
dispatchEvent<T>(event: CustomEvent<T>) {
7272
this.litElement?.dispatchEvent(event);
7373
}
74+
75+
usePropChanged(callback: Function, dependencies: string[]) {
76+
this.litElement?.usePropChanged(callback, dependencies);
77+
}
7478
}

src/utils/props.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type Props = {
88
updated: Hooks['updated'];
99
attributeChangedCallback: Hooks['attributeChangedCallback'];
1010
dispatchEvent: Hooks['dispatchEvent'],
11+
usePropChanged: Hooks['usePropChanged'],
1112
meta: LitElement,
1213
}
1314

@@ -19,6 +20,7 @@ export function generateProps(hooks: Hooks) {
1920
updated: hooks.updated.bind(hooks),
2021
attributeChangedCallback: hooks.attributeChangedCallback.bind(hooks),
2122
dispatchEvent: hooks.dispatchEvent.bind(hooks),
23+
usePropChanged: hooks.usePropChanged.bind(hooks),
2224
meta: hooks.litElement as LitElement
2325
}
2426
}

ts-component-tests/src/my-element.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { html, css, PropertyValues } from "lit";
22
import litLogo from './assets/lit.svg'
33
import viteLogo from '/vite.svg'
4-
import component, { Props } from "lit-functions";
4+
import component, { Props } from "../../src";
55

66
const style = css`
77
:host {
@@ -75,7 +75,7 @@ button:focus-visible {
7575
}
7676
`
7777

78-
function myElement({useProp, onMount, updated}: Props) {
78+
function myElement({useProp, onMount, updated, usePropChanged}: Props) {
7979
const [count, setCount] = useProp('count', {type: Number}, 0);
8080
const [refreshCounter, refresh] = useProp('refreshCounter', {type: Number}, 2);
8181
const [docs, _] = useProp('docs', {type: String}, 'This is some test docs');
@@ -88,6 +88,10 @@ function myElement({useProp, onMount, updated}: Props) {
8888
updated((changedProperties: PropertyValues) => {
8989
console.log('changed', changedProperties);
9090
});
91+
92+
usePropChanged(() => {
93+
console.log('counter changed', count)
94+
}, ['count']);
9195

9296
return html`
9397
<div>

0 commit comments

Comments
 (0)