@dinoreic/fez 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ // Global state manager with automatic component subscription
2
+ //
3
+ // Components access state via this.globalState proxy which automatically:
4
+ // - Registers component as listener when reading a value
5
+ // - Notifies component when that value changes
6
+ // - Calls onGlobalStateChange(key, value) if defined, then render()
7
+ //
8
+ // Example usage:
9
+ //
10
+ // class Counter extends FezBase {
11
+ // increment() {
12
+ // this.globalState.count = (this.globalState.count || 0) + 1
13
+ // }
14
+ //
15
+ // onGlobalStateChange(key, value) {
16
+ // console.log(`State ${key} changed to ${value}`)
17
+ // }
18
+ //
19
+ // render() {
20
+ // return `<button onclick="fez.increment()">
21
+ // Count: ${this.globalState.count || 0}
22
+ // </button>`
23
+ // }
24
+ // }
25
+ //
26
+ // External access:
27
+ // Fez.state.set('count', 10)
28
+ // Fez.state.get('count') // 10
29
+
30
+ const GlobalState = {
31
+ data: {},
32
+ listeners: new Map(), // key -> Set of components
33
+ subscribers: new Map(), // key -> Set of functions (for subscribe method)
34
+ globalSubscribers: new Set(), // Set of functions that listen to all changes
35
+
36
+ notify(key, value, oldValue) {
37
+ Fez.log(`Global state change for ${key}: ${value} (from ${oldValue})`)
38
+
39
+ // Notify component listeners
40
+ const listeners = this.listeners.get(key)
41
+ if (listeners) {
42
+ listeners.forEach(comp => {
43
+ if (comp.isConnected) {
44
+ comp.onGlobalStateChange(key, value, oldValue)
45
+ comp.render()
46
+ } else {
47
+ listeners.delete(comp)
48
+ }
49
+ })
50
+ }
51
+
52
+ // Notify key-specific subscribers
53
+ const subscribers = this.subscribers.get(key)
54
+ if (subscribers) {
55
+ subscribers.forEach(func => {
56
+ try {
57
+ func(value, oldValue, key)
58
+ } catch (error) {
59
+ console.error(`Error in subscriber for key ${key}:`, error)
60
+ }
61
+ })
62
+ }
63
+
64
+ // Notify global subscribers
65
+ this.globalSubscribers.forEach(func => {
66
+ try {
67
+ func(key, value, oldValue)
68
+ } catch (error) {
69
+ console.error(`Error in global subscriber:`, error)
70
+ }
71
+ })
72
+ },
73
+
74
+ createProxy(component) {
75
+ return new Proxy({}, {
76
+ get: (target, key) => {
77
+ // Skip if already listening to this key
78
+ component._globalStateKeys ||= new Set()
79
+ if (!component._globalStateKeys.has(key)) {
80
+ component._globalStateKeys.add(key)
81
+
82
+ if (!this.listeners.has(key)) {
83
+ this.listeners.set(key, new Set())
84
+ }
85
+ this.listeners.get(key).add(component)
86
+ }
87
+
88
+ return this.data[key]
89
+ },
90
+
91
+ set: (target, key, value) => {
92
+ const oldValue = this.data[key]
93
+ if (oldValue !== value) {
94
+ this.data[key] = value
95
+ this.notify(key, value, oldValue)
96
+ }
97
+ return true
98
+ }
99
+ })
100
+ },
101
+
102
+ // Direct methods for use outside components
103
+ set(key, value) {
104
+ const oldValue = this.data[key]
105
+ if (oldValue !== value) {
106
+ this.data[key] = value
107
+ this.notify(key, value, oldValue)
108
+ }
109
+ },
110
+
111
+ get(key) {
112
+ return this.data[key]
113
+ },
114
+
115
+ // Execute function for each component listening to a key
116
+ forEach(key, func) {
117
+ const listeners = this.listeners.get(key)
118
+ if (listeners) {
119
+ listeners.forEach(comp => {
120
+ if (comp.isConnected) {
121
+ func(comp)
122
+ } else {
123
+ listeners.delete(comp)
124
+ }
125
+ })
126
+ }
127
+ },
128
+
129
+ // Subscribe to state changes
130
+ // Usage: Fez.state.subscribe(func) - listen to all changes
131
+ // Fez.state.subscribe(key, func) - listen to specific key changes
132
+ subscribe(keyOrFunc, func) {
133
+ if (typeof keyOrFunc === 'function') {
134
+ // subscribe(func) - global subscription
135
+ this.globalSubscribers.add(keyOrFunc)
136
+ return () => this.globalSubscribers.delete(keyOrFunc)
137
+ } else {
138
+ // subscribe(key, func) - key-specific subscription
139
+ const key = keyOrFunc
140
+ if (!this.subscribers.has(key)) {
141
+ this.subscribers.set(key, new Set())
142
+ }
143
+ this.subscribers.get(key).add(func)
144
+ return () => {
145
+ const keySubscribers = this.subscribers.get(key)
146
+ if (keySubscribers) {
147
+ keySubscribers.delete(func)
148
+ if (keySubscribers.size === 0) {
149
+ this.subscribers.delete(key)
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ export default GlobalState
@@ -0,0 +1,65 @@
1
+ // Exposes node building method, that gets node name, attrs and body.
2
+ // n('span', {id: id}), n('.foo', {id: id}, body), n('.foo', {id: id}, [...])
3
+ // * you can switch places for attrs and body, and body can be list of nodes
4
+ // * n('.foo.bar') -> n('div', { class: 'foo bar' })
5
+ //
6
+ // copyright @dux, 2024
7
+ // Licence MIT
8
+
9
+ export default function n(name, attrs = {}, data) {
10
+ if (typeof attrs === 'string') {
11
+ [attrs, data] = [data, attrs]
12
+ attrs ||= {}
13
+ }
14
+
15
+ if (attrs instanceof Node) {
16
+ data = attrs
17
+ attrs = {}
18
+ }
19
+
20
+ if (Array.isArray(name)) {
21
+ data = name
22
+ name = 'div'
23
+ }
24
+
25
+ if (typeof attrs !== 'object' || Array.isArray(attrs)) {
26
+ data = attrs
27
+ attrs = {}
28
+ }
29
+
30
+ if (name.includes('.')) {
31
+ const parts = name.split('.')
32
+ name = parts.shift() || 'div'
33
+ const c = parts.join(' ');
34
+ if (attrs.class) {
35
+ attrs.class += ` ${c}`;
36
+ } else {
37
+ attrs.class = c
38
+ }
39
+ }
40
+
41
+ const node = document.createElement(name);
42
+
43
+ for (const [k, v] of Object.entries(attrs)) {
44
+ if (typeof v === 'function') {
45
+ node[k] = v.bind(this)
46
+ } else {
47
+ const value = String(v).replaceAll('fez.', this.fezHtmlRoot);
48
+ node.setAttribute(k, value)
49
+ }
50
+ }
51
+
52
+ if (data) {
53
+ if (Array.isArray(data)) {
54
+ for (const n of data) {
55
+ node.appendChild(n)
56
+ }
57
+ } else if (data instanceof Node) {
58
+ node.appendChild(data)
59
+ } else {
60
+ node.innerHTML = String(data)
61
+ }
62
+ }
63
+
64
+ return node
65
+ }
@@ -0,0 +1,119 @@
1
+ function parseBlock(data, ifStack) {
2
+ data = data
3
+ .replace(/^#?raw/, '@html')
4
+ .replace(/^#?html/, '@html')
5
+
6
+ // Handle #if directive
7
+ if (data.startsWith('#if') || data.startsWith('if')) {
8
+ ifStack.push(false)
9
+ data = data.replace(/^#?if/, '')
10
+ return `\${ ${data} ? \``
11
+ }
12
+ else if (data.startsWith('#unless') || data.startsWith('unless')) {
13
+ ifStack.push(false)
14
+ data = data.replace(/^#?unless/, '')
15
+ return `\${ !(${data}) ? \``
16
+ }
17
+ else if (data == '/block') {
18
+ return '`) && \'\'}'
19
+ }
20
+ else if (data.startsWith('#for') || data.startsWith('for')) {
21
+ data = data.replace(/^#?for/, '')
22
+ const el = data.split(' in ', 2)
23
+ return '${' + el[1] + '.map((' + el[0] + ')=>`'
24
+ }
25
+ else if (data.startsWith('#each') || data.startsWith('each')) {
26
+ data = data.replace(/^#?each/, '')
27
+ const el = data.split(' as ', 2)
28
+ return '${' + el[0] + '.map((' + el[1] + ')=>`'
29
+ }
30
+ else if (data == ':else' || data == 'else') {
31
+ ifStack[ifStack.length - 1] = true
32
+ return '` : `'
33
+ }
34
+ else if (data == '/if' || data == '/unless') {
35
+ return ifStack.pop() ? '`}' : '` : ``}'
36
+ }
37
+ else if (data == '/for' || data == '/each') {
38
+ return '`).join("")}'
39
+ }
40
+ else {
41
+ const prefix = '@html '
42
+
43
+ if (data.startsWith(prefix)) {
44
+ data = data.replace(prefix, '')
45
+ } else {
46
+ data = `Fez.htmlEscape(${data})`
47
+ }
48
+
49
+ return '${' + data + '}'
50
+ }
51
+ }
52
+
53
+ // let tpl = createTemplate(string)
54
+ // tpl({ ... this state ...})
55
+ export default function createTemplate(text, opts = {}) {
56
+ const ifStack = []
57
+
58
+ // some templating engines, as GoLangs use {{ for templates. Allow usage of [[ for fez
59
+ text = text
60
+ .replaceAll('[[', '{{')
61
+ .replaceAll(']]', '}}')
62
+
63
+ text = text.replace(/(\w+)=\{\{\s*(.*?)\s*\}\}([\s>])/g, (match, p1, p2, p3) => {
64
+ return `${p1}="{`+`{ ${p2} }`+`}"${p3}`
65
+ })
66
+
67
+ // {{block foo}} ... {{/block}}
68
+ // {{block:foo}}
69
+ const blocks = {}
70
+ text = text.replace(/\{\{block\s+(\w+)\s*\}\}([^§]+)\{\{\/block\}\}/g, (_, name, block) => {
71
+ blocks[name] = block
72
+ return ''
73
+ })
74
+ text = text.replace(/\{\{block:([\w\-]+)\s*\}\}/g, (_, name) => blocks[name] || `block:${name}?`)
75
+
76
+ // {{#for el in list }}}}
77
+ // <ui-comment :comment="el"></ui-comment>
78
+ // -> :comment="{{ JSON.stringify(el) }}"
79
+ // skip attr="foo.bar"
80
+ text = text.replace(/:(\w+)="([\w\.\[\]]+)"/, (_, m1, m2) => {
81
+ return `:${m1}=Fez.store.delete({{ Fez.store.set(${m2}) }})`
82
+ })
83
+
84
+ let result = text.replace(/{{(.*?)}}/g, (_, content) => {
85
+ content = content.replaceAll('&#x60;', '`')
86
+
87
+ content = content
88
+ .replaceAll('&lt;', '<')
89
+ .replaceAll('&gt;', '>')
90
+ .replaceAll('&amp;', '&')
91
+ const parsedData = parseBlock(content, ifStack);
92
+
93
+ return parsedData
94
+ });
95
+
96
+ result = '`' + result.trim() + '`'
97
+
98
+ try {
99
+ const funcBody = `const fez = this;
100
+ with (this) {
101
+ return ${result}
102
+ }
103
+ `
104
+ const tplFunc = new Function(funcBody);
105
+ const outFunc = (o) => {
106
+ try {
107
+ return tplFunc.bind(o)()
108
+ } catch(e) {
109
+ e.message = `FEZ template runtime error: ${e.message}\n\nTemplate source: ${result}`
110
+ console.error(e)
111
+ }
112
+ }
113
+ return outFunc
114
+ } catch(e) {
115
+ e.message = `FEZ template compile error: ${e.message}Template source:\n${result}`
116
+ console.error(e)
117
+ return ()=>Fez.error(`Template Compile Error`, true)
118
+ }
119
+ }