ripple 0.2.87 → 0.2.89

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.
@@ -142,6 +142,81 @@ describe('basic client', () => {
142
142
  expect(div.classList.contains('inactive')).toBe(true);
143
143
  });
144
144
 
145
+ it('render class attribute with array, nested array, nested object', () => {
146
+ component Basic() {
147
+ <div class={[
148
+ 'foo',
149
+ 'bar',
150
+ true && 'baz',
151
+ false && 'aaa',
152
+ null && 'bbb',
153
+ [
154
+ 'ccc',
155
+ 'ddd',
156
+ { eee: true, fff: false }
157
+ ]
158
+ ]}>
159
+ {'Class Array'}
160
+ </div>
161
+
162
+ <style>
163
+ .foo {
164
+ color: red;
165
+ }
166
+ </style>
167
+ }
168
+
169
+ render(Basic);
170
+
171
+ const div = container.querySelector('div');
172
+
173
+ expect(Array.from(div.classList).some(className => className.startsWith('ripple-'))).toBe(true);
174
+ expect(div.classList.contains('foo')).toBe(true);
175
+ expect(div.classList.contains('bar')).toBe(true);
176
+ expect(div.classList.contains('baz')).toBe(true);
177
+ expect(div.classList.contains('aaa')).toBe(false);
178
+ expect(div.classList.contains('bbb')).toBe(false);
179
+ expect(div.classList.contains('ccc')).toBe(true);
180
+ expect(div.classList.contains('ddd')).toBe(true);
181
+ expect(div.classList.contains('eee')).toBe(true);
182
+ expect(div.classList.contains('fff')).toBe(false);
183
+ });
184
+
185
+ it('render dynamic class object', () => {
186
+ component Basic() {
187
+ let active = track(false);
188
+
189
+ <button onClick={() => { @active = !@active }}>{'Toggle'}</button>
190
+ <div class={{ active: @active, inactive: !@active }}>{'Dynamic Class'}</div>
191
+
192
+ <style>
193
+ .active {
194
+ color: green;
195
+ }
196
+ </style>
197
+ }
198
+
199
+ render(Basic);
200
+
201
+ const button = container.querySelector('button');
202
+ const div = container.querySelector('div');
203
+
204
+ expect(Array.from(div.classList).some(className => className.startsWith('ripple-'))).toBe(true);
205
+ expect(div.classList.contains('inactive')).toBe(true);
206
+ expect(div.classList.contains('active')).toBe(false);
207
+
208
+ button.click();
209
+ flushSync();
210
+ expect(div.classList.contains('inactive')).toBe(false);
211
+ expect(div.classList.contains('active')).toBe(true);
212
+
213
+ button.click();
214
+ flushSync();
215
+
216
+ expect(div.classList.contains('inactive')).toBe(true);
217
+ expect(div.classList.contains('active')).toBe(false);
218
+ });
219
+
145
220
  it('render dynamic id attribute', () => {
146
221
  component Basic() {
147
222
  let count = track(0);
@@ -1547,5 +1622,20 @@ describe('basic client', () => {
1547
1622
  expect(pre1.textContent).toBe('4');
1548
1623
  expect(pre2.textContent).toBe('2');
1549
1624
  });
1625
+
1626
+ it('handles boolean props correctly', () => {
1627
+ component App() {
1628
+ <div data-disabled />
1629
+
1630
+ <Child isDisabled />
1631
+ }
1632
+
1633
+ component Child({ isDisabled }) {
1634
+ <input disabled={isDisabled} />
1635
+ }
1636
+
1637
+ render(App);
1638
+ expect(container).toMatchSnapshot();
1639
+ });
1550
1640
  });
1551
1641
 
@@ -0,0 +1,196 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, track } from 'ripple';
3
+
4
+ describe('head elements', () => {
5
+ let container;
6
+ let originalTitle;
7
+
8
+ function render(component) {
9
+ mount(component, {
10
+ target: container
11
+ });
12
+ }
13
+
14
+ beforeEach(() => {
15
+ container = document.createElement('div');
16
+ document.body.appendChild(container);
17
+ // Store original title to restore later
18
+ originalTitle = document.title;
19
+ });
20
+
21
+ afterEach(() => {
22
+ document.body.removeChild(container);
23
+ container = null;
24
+ // Restore original title
25
+ document.title = originalTitle;
26
+ });
27
+
28
+ it('renders static title element', () => {
29
+ component App() {
30
+ <head>
31
+ <title>{'Static Test Title'}</title>
32
+ </head>
33
+ <div>{'Content'}</div>
34
+ }
35
+
36
+ render(App);
37
+
38
+ expect(document.title).toBe('Static Test Title');
39
+ expect(container.querySelector('div').textContent).toBe('Content');
40
+ });
41
+
42
+ it('renders reactive title element', () => {
43
+ component App() {
44
+ let title = track('Initial Title');
45
+
46
+ <head>
47
+ <title>{@title}</title>
48
+ </head>
49
+ <div>
50
+ <button onClick={() => { @title = 'Updated Title'; }}>{'Update Title'}</button>
51
+ <span>{@title}</span>
52
+ </div>
53
+ }
54
+
55
+ render(App);
56
+
57
+ expect(document.title).toBe('Initial Title');
58
+ expect(container.querySelector('span').textContent).toBe('Initial Title');
59
+
60
+ const button = container.querySelector('button');
61
+ button.click();
62
+ flushSync();
63
+
64
+ expect(document.title).toBe('Updated Title');
65
+ expect(container.querySelector('span').textContent).toBe('Updated Title');
66
+ });
67
+
68
+ it('renders title with template literal', () => {
69
+ component App() {
70
+ let name = track('World');
71
+
72
+ <head>
73
+ <title>{`Hello ${@name}!`}</title>
74
+ </head>
75
+ <div>
76
+ <button onClick={() => { @name = 'Ripple'; }}>{'Change Name'}</button>
77
+ </div>
78
+ }
79
+
80
+ render(App);
81
+
82
+ expect(document.title).toBe('Hello World!');
83
+
84
+ const button = container.querySelector('button');
85
+ button.click();
86
+ flushSync();
87
+
88
+ expect(document.title).toBe('Hello Ripple!');
89
+ });
90
+
91
+ it('renders title with computed value', () => {
92
+ component App() {
93
+ let count = track(0);
94
+ let prefix = 'Count: ';
95
+
96
+ <head>
97
+ <title>{prefix + @count}</title>
98
+ </head>
99
+ <div>
100
+ <button onClick={() => { @count++; }}>{'Increment'}</button>
101
+ <span>{@count}</span>
102
+ </div>
103
+ }
104
+
105
+ render(App);
106
+
107
+ expect(document.title).toBe('Count: 0');
108
+
109
+ const button = container.querySelector('button');
110
+ button.click();
111
+ flushSync();
112
+
113
+ expect(document.title).toBe('Count: 1');
114
+ expect(container.querySelector('span').textContent).toBe('1');
115
+ });
116
+
117
+ it('handles multiple title updates', () => {
118
+ component App() {
119
+ let step = track(1);
120
+
121
+ <head>
122
+ <title>{`Step ${@step} of 3`}</title>
123
+ </head>
124
+ <div>
125
+ <button onClick={() => { @step = (@step % 3) + 1 }}>{'Next Step'}</button>
126
+ </div>
127
+ }
128
+
129
+ render(App);
130
+
131
+ expect(document.title).toBe('Step 1 of 3');
132
+
133
+ const button = container.querySelector('button');
134
+
135
+ button.click();
136
+ flushSync();
137
+ expect(document.title).toBe('Step 2 of 3');
138
+
139
+ button.click();
140
+ flushSync();
141
+ expect(document.title).toBe('Step 3 of 3');
142
+
143
+ button.click();
144
+ flushSync();
145
+ expect(document.title).toBe('Step 1 of 3');
146
+ });
147
+
148
+ it('renders empty title', () => {
149
+ component App() {
150
+ <head>
151
+ <title>{''}</title>
152
+ </head>
153
+ <div>{'Empty title test'}</div>
154
+ }
155
+
156
+ render(App);
157
+
158
+ expect(document.title).toBe('');
159
+ });
160
+
161
+ it('renders title with conditional content', () => {
162
+ component App() {
163
+ let showPrefix = track(true);
164
+ let title = track('Main Page');
165
+
166
+ <head>
167
+ <title>{@showPrefix ? 'App - ' + @title : @title}</title>
168
+ </head>
169
+ <div>
170
+ <button onClick={() => { @showPrefix = !@showPrefix }}>{'Toggle Prefix'}</button>
171
+ <button onClick={() => { @title = @title === 'Main Page' ? 'Settings' : 'Main Page' }}>{'Change Page'}</button>
172
+ </div>
173
+ }
174
+
175
+ render(App);
176
+
177
+ expect(document.title).toBe('App - Main Page');
178
+
179
+ const buttons = container.querySelectorAll('button');
180
+
181
+ // Toggle prefix off
182
+ buttons[0].click();
183
+ flushSync();
184
+ expect(document.title).toBe('Main Page');
185
+
186
+ // Change page
187
+ buttons[1].click();
188
+ flushSync();
189
+ expect(document.title).toBe('Settings');
190
+
191
+ // Toggle prefix back on
192
+ buttons[0].click();
193
+ flushSync();
194
+ expect(document.title).toBe('App - Settings');
195
+ });
196
+ });
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, TrackedObject, track } from 'ripple';
3
+ import { TRACKED_OBJECT } from '../../src/runtime/internal/client/constants.js';
4
+
5
+ describe('TrackedObject', () => {
6
+ let container;
7
+
8
+ function render(component) {
9
+ mount(component, {
10
+ target: container
11
+ });
12
+ }
13
+
14
+ beforeEach(() => {
15
+ container = document.createElement('div');
16
+ document.body.appendChild(container);
17
+ });
18
+
19
+ afterEach(() => {
20
+ document.body.removeChild(container);
21
+ container = null;
22
+ });
23
+
24
+ it('makes new properties reactive', () => {
25
+ component ObjectTest() {
26
+ const obj = new TrackedObject({});
27
+
28
+ obj.a = 0;
29
+
30
+ <pre>{obj.a}</pre>
31
+ <button onClick={() => { obj.a++; }}>{'Increment A'}</button>
32
+ }
33
+
34
+ render(ObjectTest);
35
+
36
+ const pre1 = container.querySelectorAll('pre')[0];
37
+ const button = container.querySelectorAll('button')[0 ];
38
+
39
+ expect(pre1.textContent).toBe('0');
40
+
41
+ button.click();
42
+ flushSync();
43
+
44
+ expect(pre1.textContent).toBe('1');
45
+ });
46
+
47
+ it('makes existing object properties reactive', () => {
48
+ component ObjectTest() {
49
+ const obj = new TrackedObject({ a: 0 });
50
+
51
+ <pre>{obj.a}</pre>
52
+ <button onClick={() => { obj.a++; }}>{'Increment A'}</button>
53
+ }
54
+
55
+ render(ObjectTest);
56
+
57
+ const pre1 = container.querySelectorAll('pre')[0];
58
+ const button = container.querySelectorAll('button')[0 ];
59
+
60
+ expect(pre1.textContent).toBe('0');
61
+
62
+ button.click();
63
+ flushSync();
64
+
65
+ expect(pre1.textContent).toBe('1');
66
+ });
67
+
68
+ it('checks if property exists via the has trap', () => {
69
+ component ObjectTest() {
70
+ const obj = new TrackedObject({b: 1});
71
+
72
+ obj.a = 0;
73
+
74
+ <pre>{'a' in obj && 'b' in obj}</pre>
75
+ }
76
+
77
+ render(ObjectTest);
78
+
79
+ const pre1 = container.querySelectorAll('pre')[0];
80
+
81
+ expect(pre1.textContent).toBe('true');
82
+ });
83
+
84
+ it('deletes properties via the delete trap', () => {
85
+ component ObjectTest() {
86
+ const obj = new TrackedObject({a: 0, b: 1});
87
+
88
+ <pre>{String(obj.a)}</pre>
89
+ <button onClick={() => { delete obj.a; }}>{'Delete A'}</button>
90
+ }
91
+
92
+ render(ObjectTest);
93
+
94
+ const pre1 = container.querySelectorAll('pre')[0];
95
+ const button = container.querySelectorAll('button')[0 ];
96
+
97
+ expect(pre1.textContent).toBe('0');
98
+
99
+ button.click();
100
+ flushSync();
101
+
102
+ expect(pre1.textContent).toBe('undefined');
103
+ });
104
+
105
+ it('checks if non-existent property is reactive when added later', () => {
106
+ component ObjectTest() {
107
+ const obj = new TrackedObject({});
108
+
109
+ <pre>{String(obj.a)} </pre>
110
+ <button onClick={() => { obj.a = 1; }}>{'Add A'}</button>
111
+ }
112
+
113
+ render(ObjectTest);
114
+
115
+ const pre1 = container.querySelectorAll('pre')[0];
116
+ const button = container.querySelectorAll('button')[0 ];
117
+
118
+ expect(pre1.textContent).toBe('undefined');
119
+
120
+ button.click();
121
+ flushSync();
122
+
123
+ expect(pre1.textContent).toBe('1');
124
+ });
125
+
126
+ it('checks that deeply nested objects are not proxied or reactive', () => {
127
+ component ObjectTest() {
128
+ const obj = new TrackedObject({ a: { b: 1 } });
129
+
130
+ <pre>{String(obj.a.b)}</pre>
131
+ <button onClick={() => { obj.a.b++; }}>{'Increment B'}</button>
132
+ }
133
+
134
+ render(ObjectTest);
135
+
136
+ const pre1 = container.querySelectorAll('pre')[0];
137
+ const button = container.querySelectorAll('button')[0 ];
138
+
139
+ expect(pre1.textContent).toBe('1');
140
+
141
+ button.click();
142
+ flushSync();
143
+
144
+ // remains unchanged
145
+ expect(pre1.textContent).toBe('1');
146
+ });
147
+
148
+ it('checks if TRACKED_OBJECT symbol is present on TrackedObject instances', () => {
149
+ component ObjectTest() {
150
+ const obj = new TrackedObject({ a: 0 });
151
+
152
+ expect(obj[TRACKED_OBJECT]).toBe(true);
153
+ }
154
+ });
155
+
156
+ it('uses the hash syntax for creating TrackedObject', () => {
157
+ component ObjectTest() {
158
+ const obj = #{ a: 0, b: 1, c: { d: {e: 8} } };
159
+
160
+ <pre>{obj.a}</pre>
161
+ <pre>{TRACKED_OBJECT in obj}</pre>
162
+ <pre>{JSON.stringify(obj)}</pre>
163
+ <button onClick={() => { obj.a++; }}>{'Increment A'}</button>
164
+ }
165
+
166
+ render(ObjectTest);
167
+
168
+ const pre1 = container.querySelectorAll('pre')[0];
169
+ const pre2 = container.querySelectorAll('pre')[1];
170
+ const pre3 = container.querySelectorAll('pre')[2];
171
+ const button = container.querySelectorAll('button')[0 ];
172
+
173
+ expect(pre1.textContent).toBe('0');
174
+ expect(pre2.textContent).toBe('true');
175
+ expect(pre3.textContent).toBe('{"a":0,"b":1,"c":{"d":{"e":8}}}');
176
+
177
+ button.click();
178
+ flushSync();
179
+
180
+ expect(pre1.textContent).toBe('1');
181
+ expect(pre3.textContent).toBe('{"a":1,"b":1,"c":{"d":{"e":8}}}');
182
+ })
183
+ });
package/types/index.d.ts CHANGED
@@ -151,3 +151,11 @@ export type TrackedObjectDeep<T> =
151
151
  : T extends object
152
152
  ? { [K in keyof T]: TrackedObjectDeep<T[K]> | Tracked<TrackedObjectDeep<T[K]>> }
153
153
  : T | Tracked<T>;
154
+
155
+ export type TrackedObject<T extends object> = T & {};
156
+
157
+ export interface TrackedObjectConstructor {
158
+ new <T extends object>(obj: T): TrackedObject<T>;
159
+ }
160
+
161
+ export declare const TrackedObject: TrackedObjectConstructor;