@rs-x/cli 2.0.0-next.2 → 2.0.0-next.21
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.
- package/README.md +5 -0
- package/bin/rsx.cjs +2868 -595
- package/package.json +5 -1
- package/{rs-x-vscode-extension-2.0.0-next.2.vsix → rs-x-vscode-extension-2.0.0-next.21.vsix} +0 -0
- package/scripts/prepare-local-rsx-packages.sh +20 -0
- package/scripts/verify-rsx-cli-mutations.sh +296 -0
- package/scripts/verify-rsx-projects.sh +220 -0
- package/scripts/verify-rsx-setup.sh +190 -0
- package/templates/angular-demo/README.md +115 -0
- package/templates/angular-demo/src/app/app.component.css +97 -0
- package/templates/angular-demo/src/app/app.component.html +58 -0
- package/templates/angular-demo/src/app/app.component.ts +52 -0
- package/templates/angular-demo/src/app/virtual-table/row-data.ts +35 -0
- package/templates/angular-demo/src/app/virtual-table/row-model.ts +45 -0
- package/templates/angular-demo/src/app/virtual-table/virtual-table-data.service.ts +136 -0
- package/templates/angular-demo/src/app/virtual-table/virtual-table-model.ts +224 -0
- package/templates/angular-demo/src/app/virtual-table/virtual-table.component.css +174 -0
- package/templates/angular-demo/src/app/virtual-table/virtual-table.component.html +50 -0
- package/templates/angular-demo/src/app/virtual-table/virtual-table.component.ts +83 -0
- package/templates/angular-demo/src/index.html +11 -0
- package/templates/angular-demo/src/main.ts +16 -0
- package/templates/angular-demo/src/styles.css +261 -0
- package/templates/next-demo/README.md +26 -0
- package/templates/next-demo/app/globals.css +431 -0
- package/templates/next-demo/app/layout.tsx +22 -0
- package/templates/next-demo/app/page.tsx +5 -0
- package/templates/next-demo/components/demo-app.tsx +114 -0
- package/templates/next-demo/components/virtual-table-row.tsx +40 -0
- package/templates/next-demo/components/virtual-table-shell.tsx +86 -0
- package/templates/next-demo/hooks/use-virtual-table-controller.ts +26 -0
- package/templates/next-demo/hooks/use-virtual-table-viewport.ts +41 -0
- package/templates/next-demo/lib/row-data.ts +35 -0
- package/templates/next-demo/lib/row-model.ts +45 -0
- package/templates/next-demo/lib/rsx-bootstrap.ts +46 -0
- package/templates/next-demo/lib/virtual-table-controller.ts +259 -0
- package/templates/next-demo/lib/virtual-table-data.service.ts +132 -0
- package/templates/react-demo/README.md +113 -0
- package/templates/react-demo/index.html +12 -0
- package/templates/react-demo/src/app/app.tsx +87 -0
- package/templates/react-demo/src/app/hooks/use-virtual-table-controller.ts +24 -0
- package/templates/react-demo/src/app/hooks/use-virtual-table-viewport.ts +39 -0
- package/templates/react-demo/src/app/virtual-table/row-data.ts +35 -0
- package/templates/react-demo/src/app/virtual-table/row-model.ts +45 -0
- package/templates/react-demo/src/app/virtual-table/virtual-table-controller.ts +259 -0
- package/templates/react-demo/src/app/virtual-table/virtual-table-data.service.ts +132 -0
- package/templates/react-demo/src/app/virtual-table/virtual-table-row.tsx +38 -0
- package/templates/react-demo/src/app/virtual-table/virtual-table-shell.tsx +84 -0
- package/templates/react-demo/src/main.tsx +24 -0
- package/templates/react-demo/src/rsx-bootstrap.ts +48 -0
- package/templates/react-demo/src/styles.css +422 -0
- package/templates/react-demo/tsconfig.json +17 -0
- package/templates/react-demo/vite.config.ts +6 -0
- package/templates/vue-demo/README.md +27 -0
- package/templates/vue-demo/src/App.vue +89 -0
- package/templates/vue-demo/src/components/VirtualTableRow.vue +33 -0
- package/templates/vue-demo/src/components/VirtualTableShell.vue +71 -0
- package/templates/vue-demo/src/composables/use-virtual-table-controller.ts +33 -0
- package/templates/vue-demo/src/composables/use-virtual-table-viewport.ts +40 -0
- package/templates/vue-demo/src/env.d.ts +10 -0
- package/templates/vue-demo/src/lib/row-data.ts +35 -0
- package/templates/vue-demo/src/lib/row-model.ts +45 -0
- package/templates/vue-demo/src/lib/rsx-bootstrap.ts +46 -0
- package/templates/vue-demo/src/lib/virtual-table-controller.ts +259 -0
- package/templates/vue-demo/src/lib/virtual-table-data.service.ts +132 -0
- package/templates/vue-demo/src/main.ts +13 -0
- package/templates/vue-demo/src/style.css +440 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/bin/zsh
|
|
2
|
+
|
|
3
|
+
set -u
|
|
4
|
+
|
|
5
|
+
unset INIT_CWD
|
|
6
|
+
unset npm_package_json
|
|
7
|
+
unset npm_package_name
|
|
8
|
+
unset npm_package_version
|
|
9
|
+
unset npm_lifecycle_event
|
|
10
|
+
unset npm_lifecycle_script
|
|
11
|
+
unset npm_config_local_prefix
|
|
12
|
+
unset npm_prefix
|
|
13
|
+
unset npm_execpath
|
|
14
|
+
unset npm_command
|
|
15
|
+
unset npm_config_node_linker
|
|
16
|
+
unset NPM_CONFIG_NODE_LINKER
|
|
17
|
+
export CI=true
|
|
18
|
+
|
|
19
|
+
script_dir="${0:A:h}"
|
|
20
|
+
package_root="${script_dir:h}"
|
|
21
|
+
rsx_cmd=(node "$package_root/bin/rsx.cjs")
|
|
22
|
+
default_base_dir="$package_root/.tests/rsx-setup-smoke"
|
|
23
|
+
prebuild_script="$script_dir/prepare-local-rsx-packages.sh"
|
|
24
|
+
|
|
25
|
+
base_dir="${RSX_SETUP_VERIFY_DIR:-$default_base_dir}"
|
|
26
|
+
work_base_dir="${RSX_SETUP_WORK_DIR:-$base_dir/workspace}"
|
|
27
|
+
pm="${RSX_PM:-npm}"
|
|
28
|
+
tag_flag="${RSX_TAG_FLAG:---next}"
|
|
29
|
+
skip_vscode_flag="${RSX_SKIP_VSCODE_FLAG:---skip-vscode}"
|
|
30
|
+
|
|
31
|
+
typeset -a summary_lines
|
|
32
|
+
overall_status=0
|
|
33
|
+
|
|
34
|
+
run_in_log() {
|
|
35
|
+
local log_file="$1"
|
|
36
|
+
shift
|
|
37
|
+
|
|
38
|
+
"$@" < /dev/null >"$log_file" 2>&1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
run_in_dir_log() {
|
|
42
|
+
local project_dir="$1"
|
|
43
|
+
local log_file="$2"
|
|
44
|
+
shift 2
|
|
45
|
+
|
|
46
|
+
(
|
|
47
|
+
cd "$project_dir" || exit 1
|
|
48
|
+
"$@" < /dev/null
|
|
49
|
+
) >"$log_file" 2>&1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
print_log_tail() {
|
|
53
|
+
local log_file="$1"
|
|
54
|
+
|
|
55
|
+
printf ' log: %s\n' "$log_file"
|
|
56
|
+
printf ' last lines:\n'
|
|
57
|
+
tail -n 20 "$log_file" | sed 's/^/ /'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
run_step() {
|
|
61
|
+
local framework="$1"
|
|
62
|
+
local step_name="$2"
|
|
63
|
+
local log_file="$3"
|
|
64
|
+
shift 3
|
|
65
|
+
|
|
66
|
+
printf '%s: %s...\n' "$framework" "$step_name"
|
|
67
|
+
if ! run_in_log "$log_file" "$@"; then
|
|
68
|
+
printf '%s: %s failed.\n' "$framework" "$step_name"
|
|
69
|
+
print_log_tail "$log_file"
|
|
70
|
+
return 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
return 0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
run_step_in_dir() {
|
|
77
|
+
local framework="$1"
|
|
78
|
+
local step_name="$2"
|
|
79
|
+
local project_dir="$3"
|
|
80
|
+
local log_file="$4"
|
|
81
|
+
shift 4
|
|
82
|
+
|
|
83
|
+
printf '%s: %s...\n' "$framework" "$step_name"
|
|
84
|
+
if ! run_in_dir_log "$project_dir" "$log_file" "$@"; then
|
|
85
|
+
printf '%s: %s failed.\n' "$framework" "$step_name"
|
|
86
|
+
print_log_tail "$log_file"
|
|
87
|
+
return 1
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
return 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
rm -rf "$base_dir"
|
|
94
|
+
mkdir -p "$base_dir"
|
|
95
|
+
mkdir -p "$work_base_dir"
|
|
96
|
+
cat > "$work_base_dir/package.json" <<'EOF'
|
|
97
|
+
{
|
|
98
|
+
"name": "rsx-setup-smoke-workspace",
|
|
99
|
+
"private": true
|
|
100
|
+
}
|
|
101
|
+
EOF
|
|
102
|
+
: > "$work_base_dir/.npmrc"
|
|
103
|
+
|
|
104
|
+
printf 'Using workspace: %s\n' "$base_dir"
|
|
105
|
+
printf 'Using project dir: %s\n' "$work_base_dir"
|
|
106
|
+
printf 'Using package manager: %s\n' "$pm"
|
|
107
|
+
printf 'Using tag flag: %s\n' "$tag_flag"
|
|
108
|
+
|
|
109
|
+
printf 'Preparing local RS-X packages...\n'
|
|
110
|
+
if ! run_in_log "$base_dir/prepare-packages.log" zsh "$prebuild_script"; then
|
|
111
|
+
printf 'Preparation failed.\n'
|
|
112
|
+
print_log_tail "$base_dir/prepare-packages.log"
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
verify_framework() {
|
|
117
|
+
local framework="$1"
|
|
118
|
+
local project_name="rsx-setup-${framework}-verify"
|
|
119
|
+
local project_dir="$work_base_dir/$project_name"
|
|
120
|
+
local project_arg="./$project_name"
|
|
121
|
+
local scaffold_log="$base_dir/${framework}-scaffold.log"
|
|
122
|
+
local install_log="$base_dir/${framework}-install.log"
|
|
123
|
+
local setup_log="$base_dir/${framework}-setup.log"
|
|
124
|
+
local setup_repeat_log="$base_dir/${framework}-setup-repeat.log"
|
|
125
|
+
local build_log="$base_dir/${framework}-build.log"
|
|
126
|
+
|
|
127
|
+
rm -rf "$project_dir"
|
|
128
|
+
|
|
129
|
+
printf '\n== %s ==\n' "$framework"
|
|
130
|
+
|
|
131
|
+
case "$framework" in
|
|
132
|
+
angular)
|
|
133
|
+
run_step_in_dir "$framework" "scaffold" "$work_base_dir" "$scaffold_log" \
|
|
134
|
+
npx @angular/cli@latest new "$project_name" --directory "$project_arg" --style css --routing --skip-git --skip-install --defaults \
|
|
135
|
+
|| { summary_lines+=("$framework: scaffold failed"); overall_status=1; return; }
|
|
136
|
+
run_step_in_dir "$framework" "install" "$project_dir" "$install_log" "$pm" install \
|
|
137
|
+
|| { summary_lines+=("$framework: install failed"); overall_status=1; return; }
|
|
138
|
+
;;
|
|
139
|
+
react)
|
|
140
|
+
run_step_in_dir "$framework" "scaffold" "$work_base_dir" "$scaffold_log" \
|
|
141
|
+
npx create-vite@latest "$project_arg" --no-interactive --template react-ts \
|
|
142
|
+
|| { summary_lines+=("$framework: scaffold failed"); overall_status=1; return; }
|
|
143
|
+
run_step_in_dir "$framework" "install" "$project_dir" "$install_log" "$pm" install \
|
|
144
|
+
|| { summary_lines+=("$framework: install failed"); overall_status=1; return; }
|
|
145
|
+
;;
|
|
146
|
+
vue)
|
|
147
|
+
run_step_in_dir "$framework" "scaffold" "$work_base_dir" "$scaffold_log" \
|
|
148
|
+
npx create-vite@latest "$project_arg" --no-interactive --template vue-ts \
|
|
149
|
+
|| { summary_lines+=("$framework: scaffold failed"); overall_status=1; return; }
|
|
150
|
+
run_step_in_dir "$framework" "install" "$project_dir" "$install_log" "$pm" install \
|
|
151
|
+
|| { summary_lines+=("$framework: install failed"); overall_status=1; return; }
|
|
152
|
+
;;
|
|
153
|
+
next)
|
|
154
|
+
run_step_in_dir "$framework" "scaffold" "$work_base_dir" "$scaffold_log" \
|
|
155
|
+
npx create-next-app@latest "$project_arg" --ts --app --empty --use-npm --yes --disable-git \
|
|
156
|
+
|| { summary_lines+=("$framework: scaffold failed"); overall_status=1; return; }
|
|
157
|
+
;;
|
|
158
|
+
*)
|
|
159
|
+
printf 'Unknown framework: %s\n' "$framework"
|
|
160
|
+
summary_lines+=("$framework: unsupported")
|
|
161
|
+
overall_status=1
|
|
162
|
+
return
|
|
163
|
+
;;
|
|
164
|
+
esac
|
|
165
|
+
|
|
166
|
+
run_step_in_dir "$framework" "rsx setup" "$project_dir" "$setup_log" "${rsx_cmd[@]}" setup --pm "$pm" "$tag_flag" "$skip_vscode_flag" \
|
|
167
|
+
|| { summary_lines+=("$framework: rsx setup failed"); overall_status=1; return; }
|
|
168
|
+
|
|
169
|
+
run_step_in_dir "$framework" "rsx setup (repeat)" "$project_dir" "$setup_repeat_log" "${rsx_cmd[@]}" setup --pm "$pm" "$tag_flag" "$skip_vscode_flag" \
|
|
170
|
+
|| { summary_lines+=("$framework: repeat rsx setup failed"); overall_status=1; return; }
|
|
171
|
+
|
|
172
|
+
run_step_in_dir "$framework" "build" "$project_dir" "$build_log" "$pm" run build \
|
|
173
|
+
|| { summary_lines+=("$framework: build failed"); overall_status=1; return; }
|
|
174
|
+
|
|
175
|
+
printf '%s: build passed.\n' "$framework"
|
|
176
|
+
summary_lines+=("$framework: pass")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
cd "$base_dir" || exit 1
|
|
180
|
+
verify_framework angular
|
|
181
|
+
verify_framework react
|
|
182
|
+
verify_framework vue
|
|
183
|
+
verify_framework next
|
|
184
|
+
|
|
185
|
+
printf '\nSummary\n'
|
|
186
|
+
for line in "${summary_lines[@]}"; do
|
|
187
|
+
printf ' %s\n' "$line"
|
|
188
|
+
done
|
|
189
|
+
|
|
190
|
+
exit "$overall_status"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# rsx-angular-example
|
|
2
|
+
|
|
3
|
+
Angular demo app for RS-X.
|
|
4
|
+
|
|
5
|
+
**Website & docs:** [rsxjs.com](https://www.rsxjs.com/)
|
|
6
|
+
|
|
7
|
+
This example shows a million-row virtual table that:
|
|
8
|
+
|
|
9
|
+
- uses the `rsx` pipe from `@rs-x/angular`
|
|
10
|
+
- creates row expressions with `rsx(...)`
|
|
11
|
+
- keeps a fixed pool of row models and expressions
|
|
12
|
+
- loads pages on demand while scrolling
|
|
13
|
+
- keeps memory bounded by reusing the row pool and pruning old page data
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cd rsx-angular-example
|
|
19
|
+
npm install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm start
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`npm start` first runs the RS-X build step, then starts Angular.
|
|
29
|
+
|
|
30
|
+
## Build
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm run build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This runs:
|
|
37
|
+
|
|
38
|
+
1. `rsx build --project tsconfig.json --no-emit --prod`
|
|
39
|
+
2. `ng build`
|
|
40
|
+
|
|
41
|
+
So the example gets:
|
|
42
|
+
|
|
43
|
+
- RS-X semantic checks
|
|
44
|
+
- generated AOT RS-X caches
|
|
45
|
+
- Angular production build output
|
|
46
|
+
|
|
47
|
+
## Basic RS-X Angular setup
|
|
48
|
+
|
|
49
|
+
The example uses the normal Angular RS-X setup:
|
|
50
|
+
|
|
51
|
+
### 1. Register RS-X providers at bootstrap
|
|
52
|
+
|
|
53
|
+
In `src/main.ts`:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { bootstrapApplication } from '@angular/platform-browser';
|
|
57
|
+
import { providexRsx } from '@rs-x/angular';
|
|
58
|
+
|
|
59
|
+
bootstrapApplication(AppComponent, {
|
|
60
|
+
providers: [...providexRsx()],
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Create expressions with `rsx(...)`
|
|
65
|
+
|
|
66
|
+
In `src/app/virtual-table/row-model.ts`:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
idExpr: rsx<number>('id')(model),
|
|
70
|
+
nameExpr: rsx<string>('name')(model),
|
|
71
|
+
totalExpr: rsx<number>('price * quantity')(model),
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3. Bind them with `RsxPipe`
|
|
75
|
+
|
|
76
|
+
In `src/app/virtual-table/virtual-table.component.html`:
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<div *ngFor="let item of state.rowsExpression | rsx; trackBy: trackByIndex">
|
|
80
|
+
<span>{{ item.row.nameExpr | rsx }}</span>
|
|
81
|
+
<span>{{ item.row.totalExpr | rsx }}</span>
|
|
82
|
+
</div>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Why this example is useful
|
|
86
|
+
|
|
87
|
+
The point of the demo is not just rendering a table. It shows how RS-X behaves in a realistic Angular scenario:
|
|
88
|
+
|
|
89
|
+
- large logical dataset: `1,000,000` rows
|
|
90
|
+
- small live expression pool: only the pooled row models stay active
|
|
91
|
+
- page loading is async to simulate real server requests
|
|
92
|
+
- old loaded pages are pruned so scrolling does not grow memory forever
|
|
93
|
+
|
|
94
|
+
## About the `rsx` pipe in this demo
|
|
95
|
+
|
|
96
|
+
This example uses the `rsx` pipe directly in the template so the RS-X behavior is easy to see.
|
|
97
|
+
|
|
98
|
+
That is a demo choice, not a restriction.
|
|
99
|
+
|
|
100
|
+
In a real Angular app, you can also adapt RS-X values into standard Angular constructs such as signals if that fits your component architecture better.
|
|
101
|
+
|
|
102
|
+
## Key files
|
|
103
|
+
|
|
104
|
+
- `src/main.ts`
|
|
105
|
+
- `src/app/app.component.ts`
|
|
106
|
+
- `src/app/app.component.html`
|
|
107
|
+
- `src/app/virtual-table/virtual-table.component.ts`
|
|
108
|
+
- `src/app/virtual-table/virtual-table.component.html`
|
|
109
|
+
- `src/app/virtual-table/virtual-table-model.ts`
|
|
110
|
+
- `src/app/virtual-table/virtual-table-data.service.ts`
|
|
111
|
+
- `src/app/virtual-table/row-model.ts`
|
|
112
|
+
|
|
113
|
+
## Notes
|
|
114
|
+
|
|
115
|
+
- The virtual table uses a bounded pool and bounded page retention on purpose, so performance characteristics stay visible while memory stays under control.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
min-height: 100vh;
|
|
4
|
+
color: var(--text);
|
|
5
|
+
background: transparent;
|
|
6
|
+
transition: color 180ms ease;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.app-shell {
|
|
10
|
+
max-width: 1120px;
|
|
11
|
+
margin: 0 auto;
|
|
12
|
+
padding: 32px 24px 72px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.app-header {
|
|
16
|
+
margin-bottom: 24px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.app-header-top {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: flex-start;
|
|
22
|
+
justify-content: space-between;
|
|
23
|
+
gap: 20px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.app-eyebrow {
|
|
27
|
+
letter-spacing: 0.2em;
|
|
28
|
+
text-transform: uppercase;
|
|
29
|
+
font-size: 12px;
|
|
30
|
+
color: var(--brand);
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
margin-bottom: 8px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.app-header h1 {
|
|
36
|
+
margin: 0 0 12px;
|
|
37
|
+
font-size: 34px;
|
|
38
|
+
line-height: 1.08;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.app-subtitle {
|
|
42
|
+
margin: 0;
|
|
43
|
+
color: var(--muted);
|
|
44
|
+
max-width: 640px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.app-panel {
|
|
48
|
+
background: var(--surface);
|
|
49
|
+
border: 1px solid var(--border-soft);
|
|
50
|
+
border-radius: 24px;
|
|
51
|
+
padding: 24px;
|
|
52
|
+
box-shadow: var(--shadow-2);
|
|
53
|
+
backdrop-filter: blur(12px);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.theme-toggle {
|
|
57
|
+
border: 1px solid var(--border);
|
|
58
|
+
background: linear-gradient(
|
|
59
|
+
135deg,
|
|
60
|
+
color-mix(in srgb, var(--surface-solid) 92%, var(--brand) 8%),
|
|
61
|
+
color-mix(in srgb, var(--surface-solid) 92%, var(--brand-2) 8%)
|
|
62
|
+
);
|
|
63
|
+
color: var(--text);
|
|
64
|
+
border-radius: 999px;
|
|
65
|
+
padding: 10px 14px;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
box-shadow: var(--shadow-1);
|
|
68
|
+
transition:
|
|
69
|
+
transform 160ms ease,
|
|
70
|
+
border-color 160ms ease,
|
|
71
|
+
background 160ms ease;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.theme-toggle:hover {
|
|
75
|
+
transform: translateY(-1px);
|
|
76
|
+
border-color: var(--focus);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.theme-toggle:focus-visible {
|
|
80
|
+
outline: 2px solid var(--focus);
|
|
81
|
+
outline-offset: 2px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@media (max-width: 760px) {
|
|
85
|
+
.app-shell {
|
|
86
|
+
padding: 24px 16px 48px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.app-header-top {
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
align-items: stretch;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.theme-toggle {
|
|
95
|
+
align-self: flex-start;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<main class="app-shell">
|
|
2
|
+
<section class="hero">
|
|
3
|
+
<div class="container">
|
|
4
|
+
<div class="heroGrid">
|
|
5
|
+
<div class="heroLeft">
|
|
6
|
+
<p class="app-eyebrow">RS-X Angular Demo</p>
|
|
7
|
+
<h1 class="hTitle">Virtual Table</h1>
|
|
8
|
+
<p class="hSubhead">
|
|
9
|
+
Million-row scrolling with a fixed RS-X expression pool.
|
|
10
|
+
</p>
|
|
11
|
+
<p class="hSub">
|
|
12
|
+
This demo keeps rendering bounded while streaming pages on demand,
|
|
13
|
+
so scrolling stays smooth without growing expression memory with the
|
|
14
|
+
dataset.
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<div class="heroActions">
|
|
18
|
+
<a
|
|
19
|
+
class="btn btnGhost"
|
|
20
|
+
href="https://www.rsxjs.com/"
|
|
21
|
+
target="_blank"
|
|
22
|
+
rel="noreferrer"
|
|
23
|
+
>
|
|
24
|
+
rs-x
|
|
25
|
+
</a>
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
class="btn btnGhost theme-toggle"
|
|
29
|
+
(click)="toggleTheme()"
|
|
30
|
+
[attr.aria-label]="
|
|
31
|
+
'Switch to ' + (theme === 'dark' ? 'light' : 'dark') + ' mode'
|
|
32
|
+
"
|
|
33
|
+
>
|
|
34
|
+
{{ theme === 'dark' ? 'Light mode' : 'Dark mode' }}
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<aside class="card heroNote">
|
|
40
|
+
<h2 class="cardTitle">What This Shows</h2>
|
|
41
|
+
<p class="cardText">
|
|
42
|
+
Only a small row-model pool stays alive while pages stream in around
|
|
43
|
+
the viewport. That means one million logical rows without one
|
|
44
|
+
million live bindings.
|
|
45
|
+
</p>
|
|
46
|
+
</aside>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<section class="section">
|
|
52
|
+
<div class="container">
|
|
53
|
+
<section class="app-panel card">
|
|
54
|
+
<rsx-virtual-table></rsx-virtual-table>
|
|
55
|
+
</section>
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
</main>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DOCUMENT } from '@angular/common';
|
|
2
|
+
import {
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
Component,
|
|
5
|
+
HostBinding,
|
|
6
|
+
inject,
|
|
7
|
+
OnInit,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
|
|
10
|
+
import { VirtualTableComponent } from './virtual-table/virtual-table.component';
|
|
11
|
+
|
|
12
|
+
type ThemeMode = 'light' | 'dark';
|
|
13
|
+
|
|
14
|
+
@Component({
|
|
15
|
+
selector: 'app-root',
|
|
16
|
+
standalone: true,
|
|
17
|
+
imports: [VirtualTableComponent],
|
|
18
|
+
templateUrl: './app.component.html',
|
|
19
|
+
styleUrls: ['./app.component.css'],
|
|
20
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
+
})
|
|
22
|
+
export class AppComponent implements OnInit {
|
|
23
|
+
private readonly _document = inject(DOCUMENT);
|
|
24
|
+
@HostBinding('class.theme-dark') public isDarkTheme = false;
|
|
25
|
+
public theme: ThemeMode = 'dark';
|
|
26
|
+
|
|
27
|
+
public ngOnInit(): void {
|
|
28
|
+
this.theme = this.getInitialTheme();
|
|
29
|
+
this.applyTheme(this.theme);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public toggleTheme(): void {
|
|
33
|
+
this.theme = this.theme === 'dark' ? 'light' : 'dark';
|
|
34
|
+
this.applyTheme(this.theme);
|
|
35
|
+
window.localStorage.setItem('rsx-theme', this.theme);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private getInitialTheme(): ThemeMode {
|
|
39
|
+
const storedTheme = window.localStorage.getItem('rsx-theme');
|
|
40
|
+
if (storedTheme === 'light' || storedTheme === 'dark') {
|
|
41
|
+
return storedTheme;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return 'dark';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private applyTheme(theme: ThemeMode): void {
|
|
48
|
+
this.isDarkTheme = theme === 'dark';
|
|
49
|
+
this._document.documentElement.setAttribute('data-theme', theme);
|
|
50
|
+
this._document.body.setAttribute('data-theme', theme);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type SortKey = 'id' | 'name' | 'price' | 'quantity' | 'category';
|
|
2
|
+
export type SortDirection = 'asc' | 'desc';
|
|
3
|
+
|
|
4
|
+
export type RowData = {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
price: number;
|
|
8
|
+
quantity: number;
|
|
9
|
+
category: string;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const categories = ['Hardware', 'Software', 'Design', 'Ops'];
|
|
14
|
+
|
|
15
|
+
function pad(value: number): string {
|
|
16
|
+
return value.toString().padStart(2, '0');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createRowData(id: number): RowData {
|
|
20
|
+
const zeroBasedId = id - 1;
|
|
21
|
+
const price = 25 + (zeroBasedId % 1000) / 10;
|
|
22
|
+
const quantity = 1 + (Math.floor(zeroBasedId / 1000) % 100);
|
|
23
|
+
const category = categories[zeroBasedId % categories.length] ?? 'General';
|
|
24
|
+
const month = pad(((zeroBasedId * 7) % 12) + 1);
|
|
25
|
+
const day = pad(((zeroBasedId * 11) % 28) + 1);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
name: `Product ${id.toString().padStart(7, '0')}`,
|
|
30
|
+
price,
|
|
31
|
+
quantity,
|
|
32
|
+
category,
|
|
33
|
+
updatedAt: `2026-${month}-${day}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type IExpression, rsx } from '@rs-x/expression-parser';
|
|
2
|
+
|
|
3
|
+
import type { RowData } from './row-data';
|
|
4
|
+
|
|
5
|
+
export type RowModel = {
|
|
6
|
+
model: RowData;
|
|
7
|
+
idExpr: IExpression<number>;
|
|
8
|
+
nameExpr: IExpression<string>;
|
|
9
|
+
categoryExpr: IExpression<string>;
|
|
10
|
+
priceExpr: IExpression<number>;
|
|
11
|
+
quantityExpr: IExpression<number>;
|
|
12
|
+
updatedAtExpr: IExpression<string>;
|
|
13
|
+
totalExpr: IExpression<number>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function createRowModel(): RowModel {
|
|
17
|
+
const model: RowData = {
|
|
18
|
+
id: 0,
|
|
19
|
+
name: '',
|
|
20
|
+
price: 0,
|
|
21
|
+
quantity: 0,
|
|
22
|
+
category: 'General',
|
|
23
|
+
updatedAt: '2026-01-01',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
model,
|
|
28
|
+
idExpr: rsx<number>('id')(model),
|
|
29
|
+
nameExpr: rsx<string>('name')(model),
|
|
30
|
+
categoryExpr: rsx<string>('category')(model),
|
|
31
|
+
priceExpr: rsx<number>('price')(model),
|
|
32
|
+
quantityExpr: rsx<number>('quantity')(model),
|
|
33
|
+
updatedAtExpr: rsx<string>('updatedAt')(model),
|
|
34
|
+
totalExpr: rsx<number>('price * quantity')(model),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function updateRowModel(target: RowModel, data: RowData): void {
|
|
39
|
+
target.model.id = data.id;
|
|
40
|
+
target.model.name = data.name;
|
|
41
|
+
target.model.price = data.price;
|
|
42
|
+
target.model.quantity = data.quantity;
|
|
43
|
+
target.model.category = data.category;
|
|
44
|
+
target.model.updatedAt = data.updatedAt;
|
|
45
|
+
}
|