Skip to content

Commit e83da80

Browse files
committed
Improved tracking of deep objects
1 parent 87c28a7 commit e83da80

10 files changed

Lines changed: 235 additions & 137 deletions

File tree

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ const hello = state({
7171
world: 'example'
7272
}, { deep: true })
7373

74-
watch([track(() => hello.value)], () => {
74+
// only refreshes when hello changes
75+
watch([track(() => hello.value.hello)], () => {
7576
console.log('Hello changed')
7677
})
7778

@@ -94,6 +95,31 @@ html`
9495
<${ExampleComponent} example="hello world" />
9596
`
9697
```
98+
99+
#### Function Components Life-Cycle hooks
100+
101+
```ts
102+
import {defineEmits, defineProps, defineSlot, html, onMounted} from "pulsjs";
103+
104+
function ExampleComponent() {
105+
const props = defineProps<{
106+
example: string;
107+
}>()
108+
const emit = defineEmits()
109+
const slot = defineSlot<Node[]>()
110+
111+
onMounted(() => {
112+
console.log('Mounted')
113+
})
114+
115+
onUnmounted(() => {
116+
console.log('Unmounted')
117+
})
118+
119+
return html``
120+
}
121+
```
122+
97123
### Class components
98124
```js
99125
import { html } from 'pulsjs'

packages/puls-adapter/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './src/PulsAdapter';
2-
export * from './src/lifecycle-hooks';
2+
export * from './src/lifecycle-hooks';
3+
export * from './src/lifecycle-defines';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export type EmitFunction = (name: string, ...args: any[]) => any
2+
3+
export const currentLifecycleDefines = {
4+
props: {},
5+
emitsFunction: undefined as EmitFunction|undefined,
6+
exports: {} as Record<string, any>,
7+
slot: undefined as any
8+
}
9+
10+
export function defineProps<T>(): T {
11+
return currentLifecycleDefines.props as T
12+
}
13+
14+
15+
export function defineEmits<T extends Record<string, ((...args: any[]) => any)>>(
16+
): <K extends keyof T>(event: K, ...args: Parameters<T[K]>) => T[K] {
17+
return currentLifecycleDefines.emitsFunction! as any
18+
}
19+
20+
export function defineExports(exprts: any): any {
21+
currentLifecycleDefines.exports = exprts
22+
}
23+
24+
export function defineSlot<T>(): T {
25+
return currentLifecycleDefines.slot
26+
}

packages/puls-dom-adapter/src/PulsDOMAdapter.ts

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
OnUnmount,
77
OnUnmounted,
88
PulsAdapter,
9-
resetLifecycleHooks
9+
resetLifecycleHooks,
10+
currentLifecycleDefines
1011
} from "pulsjs-adapter";
1112

1213
export type ValueTransformer<T> = {
@@ -28,6 +29,25 @@ export class PulsDOMAdapter extends PulsAdapter<Node[]>{
2829
return this.documentOverride ?? window.document
2930
}
3031

32+
setLifecycleDefines(tag: any, attributes: Record<string, any>, slot: any) {
33+
currentLifecycleDefines.exports = {}
34+
currentLifecycleDefines.props = attributes
35+
currentLifecycleDefines.slot = slot
36+
currentLifecycleDefines.emitsFunction = (name: string, ...args: any) => {
37+
const fn = attributes[`@${name}`]
38+
if (fn) {
39+
return fn(...args)
40+
}
41+
}
42+
}
43+
44+
clearLifecycleDefines() {
45+
currentLifecycleDefines.emitsFunction = undefined
46+
currentLifecycleDefines.props = {}
47+
currentLifecycleDefines.emitsFunction = undefined
48+
currentLifecycleDefines.slot = undefined
49+
}
50+
3151
partTransformers: Record<string, (part: ParserOutput) => any> = {
3252
'text': (part) => [this.document.createTextNode((part as ParserText).value)],
3353
'element': (part) => {
@@ -39,16 +59,34 @@ export class PulsDOMAdapter extends PulsAdapter<Node[]>{
3959
if ('prototype' in conf.tag && conf.tag.prototype instanceof HTMLElement) {
4060
out = this.createFromValue(this.createElement(conf))
4161
} else {
42-
out = this.createFromValue((conf.tag as (values: any) => any)({
43-
...conf.attributes.reduce((acc, [key, value]) => ({
44-
...acc,
45-
[key]: value
46-
}), {}),
62+
// Function Components Implementation
63+
64+
for (const [key, value] of conf.attributes) {
65+
if (key === ':if' || key === ':else-if' || key === ':else') {
66+
return this.setConditionFlowAttribute(key, value, conf)
67+
}
68+
}
69+
70+
const attributes: Record<string, any> = conf.attributes.reduce((acc, [key, value]) => ({
71+
...acc,
72+
[key]: value
73+
}), {})
4774

48-
...(conf.attributes.find(([k]) => k === ':props')?.[1] || {}),
75+
const slot = (conf.body.length > 0 ? (new (this.constructor as new (b: ParserOutput[]) => PulsDOMAdapter)(conf.body)).render() : undefined)
4976

50-
$slot: (conf.body.length > 0 ? (new (this.constructor as new (b: ParserOutput[]) => PulsDOMAdapter)(conf.body)).render() : undefined),
77+
this.setLifecycleDefines(conf.tag, attributes, slot)
78+
79+
// Call function component
80+
out = this.createFromValue((conf.tag as (values: any) => any)({
81+
...attributes,
82+
$slot: slot
5183
}))
84+
85+
this.clearLifecycleDefines()
86+
87+
if (':ref' in attributes) {
88+
attributes[':ref'](...(out || []), currentLifecycleDefines.exports)
89+
}
5290
}
5391

5492
let lifeCycleComment: undefined|Comment= undefined
@@ -181,41 +219,45 @@ export class PulsDOMAdapter extends PulsAdapter<Node[]>{
181219
}
182220
}
183221

184-
setAttribute(el: Element|undefined, key: string, value: any, parserTag: ParserTag): Node|undefined {
185-
if (key === ':if' || key === ':else-if' || key === ':else') {
186-
if (key === ':if') {
187-
this.currentControlFlow = this.controlFlows.push([value]) - 1
188-
if (!value) {
189-
return this.document.createComment('if')
190-
}
222+
setConditionFlowAttribute(key: string, value: any, parserTag: ParserTag) {
223+
if (key === ':if') {
224+
this.currentControlFlow = this.controlFlows.push([value]) - 1
225+
if (!value) {
226+
return this.document.createComment('if')
227+
}
228+
}
229+
if (key === ':else-if') {
230+
if (typeof this.controlFlows[this.currentControlFlow] === 'undefined') {
231+
throw new Error('else-if without if')
191232
}
192-
if (key === ':else-if') {
193-
if (typeof this.controlFlows[this.currentControlFlow] === 'undefined') {
194-
throw new Error('else-if without if')
195-
}
196233

197-
let isElse = !this.controlFlows[this.currentControlFlow].includes(true)
198-
this.controlFlows[this.currentControlFlow].push(value)
234+
let isElse = !this.controlFlows[this.currentControlFlow].includes(true)
235+
this.controlFlows[this.currentControlFlow].push(value)
199236

200-
if (!(isElse && value)) {
201-
return this.document.createComment('if')
202-
}
237+
if (!(isElse && value)) {
238+
return this.document.createComment('if')
239+
}
240+
}
241+
if (key === ':else') {
242+
if (typeof this.controlFlows[this.currentControlFlow] === 'undefined') {
243+
throw new Error('else without if before')
203244
}
204-
if (key === ':else') {
205-
if (typeof this.controlFlows[this.currentControlFlow] === 'undefined') {
206-
throw new Error('else without if before')
207-
}
208245

209-
let isElse = !this.controlFlows[this.currentControlFlow].includes(true)
246+
let isElse = !this.controlFlows[this.currentControlFlow].includes(true)
210247

211-
if (!isElse) {
212-
return this.document.createComment('if')
213-
}
248+
if (!isElse) {
249+
return this.document.createComment('if')
214250
}
215-
return this.createElement({
216-
...parserTag,
217-
attributes: parserTag.attributes.filter(([k]) => k !== key)
218-
})
251+
}
252+
return this.createElement({
253+
...parserTag,
254+
attributes: parserTag.attributes.filter(([k]) => k !== key)
255+
})
256+
}
257+
258+
setAttribute(el: Element|undefined, key: string, value: any, parserTag: ParserTag): Node|undefined {
259+
if (key === ':if' || key === ':else-if' || key === ':else') {
260+
return this.setConditionFlowAttribute(key, value, parserTag)
219261
}
220262

221263
if (el === undefined) return;

packages/puls-dom-adapter/src/PulsHookedDOMAdapter.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {PulsDOMAdapter} from "./PulsDOMAdapter";
2-
import {computed, Hook} from "pulsjs-state";
2+
import {computed, Hook, isHook, track} from "pulsjs-state";
33
import {ParserTag} from "pulsjs-template";
44

55
// Adds support for hooks in the PulsDOMAdapter
@@ -21,18 +21,27 @@ export class PulsHookedDOMAdapter extends PulsDOMAdapter {
2121
if (el && key.startsWith(':bind')) {
2222
const field = key.split(':')[2] || 'value'
2323

24+
if (typeof value === 'function')
25+
value = track(value)
26+
27+
if (!isHook(value)) throw new Error(`Expected a hook on :bind. Got ${typeof value}`)
28+
2429
const hook = value as Hook<any>
2530

26-
hook.addListener(() => {
27-
(el as any)[field] = hook.value
28-
})
31+
let removeListener: any;
32+
const setListener = () => {
33+
removeListener = hook.addListener(() => {
34+
(el as any)[field] = hook.value
35+
})
36+
}
37+
setListener()
2938

3039
el.addEventListener(field === 'value' ? 'input' : `input:${field}`, () => {
31-
hook.setValue((el as any)[field])
40+
removeListener()
41+
hook.setValue((el as any)[field]);
42+
setListener()
3243
})
3344
;(el as any)[field] = hook.value
34-
35-
3645
return
3746
}
3847

@@ -92,14 +101,14 @@ export class PulsHookedDOMAdapter extends PulsDOMAdapter {
92101
for (let c of this.controlFlows[currentControlFlowId]) {
93102
if (c) return false;
94103
}
95-
return value instanceof Hook ? value.value : value
104+
return isHook(value) ? value.value : value
96105
}
97106

98107
const ind = this.controlFlows[currentControlFlowId].push(cond()) - 1
99108

100-
this.controlFlowHooks[this.currentControlFlow][ind] = value instanceof Hook ? value : null
109+
this.controlFlowHooks[this.currentControlFlow][ind] = isHook(value) ? value : null
101110

102-
if (value && value instanceof Hook) {
111+
if (value && isHook(value)) {
103112
for (const hk of this.controlFlowHooks[this.currentControlFlow]) {
104113
hk?.addListener(() => {
105114
this.controlFlows[currentControlFlowId][ind] = cond()
@@ -110,13 +119,13 @@ export class PulsHookedDOMAdapter extends PulsDOMAdapter {
110119
return createIfListener(
111120
[
112121
...this.controlFlowHooks[this.currentControlFlow].map((c) => c).filter(c => c !== null),
113-
...(value instanceof Hook ? [value] : [])
122+
...(isHook(value) ? [value] : [])
114123
],
115124
() => this.controlFlows[currentControlFlowId][ind]
116125
)
117126
}
118127

119-
if (key.startsWith('@') || !(value instanceof Hook))
128+
if (key.startsWith('@') || !isHook(value))
120129
return super.setAttribute(el, key, value, parserTag);
121130

122131
const hook = value as Hook<any>
@@ -142,6 +151,11 @@ export class PulsHookedDOMAdapter extends PulsDOMAdapter {
142151
)
143152
}
144153

154+
if (el && '__puls_inject_hooks_as_value' in el) {
155+
super.setAttribute(el, key, hook, parserTag)
156+
return;
157+
}
158+
145159
const listener = () => {
146160
super.setAttribute(el, key, hook.value, parserTag)
147161
}
@@ -152,7 +166,7 @@ export class PulsHookedDOMAdapter extends PulsDOMAdapter {
152166
}
153167

154168
createFromValue(value: any) : undefined|Node[] {
155-
if (!(value instanceof Hook))
169+
if (!isHook(value))
156170
return super.createFromValue(value);
157171

158172
const hook = value as Hook<any>
@@ -218,7 +232,7 @@ export class PulsHookedDOMAdapter extends PulsDOMAdapter {
218232

219233

220234
setElementStyle(el: Element, key: string, value: any) {
221-
if (value instanceof Hook) {
235+
if (isHook(value)) {
222236
this.addListener(el, () => {
223237
super.setElementClass(el, key, value.value)
224238
})
@@ -230,7 +244,7 @@ export class PulsHookedDOMAdapter extends PulsDOMAdapter {
230244
}
231245

232246
setElementClass(el: Element, key: string, condition: any) {
233-
if (condition instanceof Hook) {
247+
if (isHook(condition)) {
234248
this.addListener(el, condition.addListener(() => {
235249
super.setElementClass(el, key, condition.value)
236250
}))

packages/puls-jsx/jsx-runtime/index.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@ export function h(type: any, props?: any, children: any[] = []) {
66
props = {}
77
}
88

9-
if (props && props.children) {
9+
if (props && 'children' in props) {
1010
children = props.children;
1111
delete props.children;
1212
}
13+
if (props && 'slot' in props) {
14+
delete props.slot;
15+
}
16+
17+
1318

14-
return html`<${type} :props=${
19+
return html`<${type} ${
1520
Object.entries(props || {})
1621
.map(([key, value]) => {
1722
if (key.startsWith('on:')) {
@@ -20,6 +25,9 @@ export function h(type: any, props?: any, children: any[] = []) {
2025
if (key === 'classList' || key === 'className') {
2126
key = 'class'
2227
}
28+
if (key === 'p:ref') {
29+
key = ':ref'
30+
}
2331
if (key === 'p:bind') {
2432
key = ':bind'
2533
}
@@ -40,10 +48,18 @@ export function h(type: any, props?: any, children: any[] = []) {
4048
}
4149

4250
export function Fragment(...a: any) {
43-
console.log(a)
44-
console.log('fragment called --------------------------------')
51+
return a
4552
}
4653

4754
export const jsx = h
4855
export const jsxs = h
49-
export const jsxDEV = h
56+
export const jsxDEV = h
57+
58+
export type Props<T> = {
59+
[K in `on:${string}`]?: (...args: any[]) => any;
60+
} & T & {
61+
'p:ref'?: (exports: Record<string, any>, el: HTMLElement) => void;
62+
'p:if'?: any;
63+
'p:else-if'?: any;
64+
'p:else'?: any;
65+
}

0 commit comments

Comments
 (0)