Merge pull request 'feat: add runtime and alter parser' (#7) from iandx/inula:API-2.0 into API-2.0
This commit is contained in:
commit
cd9bddbe10
|
@ -0,0 +1,55 @@
|
||||||
|
```js
|
||||||
|
function MyComp({prop1}) {
|
||||||
|
let count = 1
|
||||||
|
let doubleCount = count * 2
|
||||||
|
|
||||||
|
const updateCount = () => {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>{prop1}</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
function MyComp() {
|
||||||
|
let prop1$$prop
|
||||||
|
|
||||||
|
let count = 1
|
||||||
|
let doubleCount = count * 2
|
||||||
|
let update$$doubleCount = () => {
|
||||||
|
if (cached()) return
|
||||||
|
doubleCount = count * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCount = () => {
|
||||||
|
viewModel.update(count++)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----
|
||||||
|
const node = createElement('div')
|
||||||
|
setProps(node, "textContent", [prop1])
|
||||||
|
|
||||||
|
const viewModel = Inula.createElement({
|
||||||
|
node,
|
||||||
|
updateProp: (propName, value) => {
|
||||||
|
if (propName === 'prop1') {
|
||||||
|
prop1$$prop = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateState: (changed) => {
|
||||||
|
if (changed & 0x1) {
|
||||||
|
update$$doubleCount()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateView: (changed) => {
|
||||||
|
if (changed & 0x1) {
|
||||||
|
setProps(node, "textContent", [prop1$$prop])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return viewModel
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -12,14 +12,14 @@
|
||||||
"@babel/standalone": "^7.22.4",
|
"@babel/standalone": "^7.22.4",
|
||||||
"@openinula/next": "workspace:*",
|
"@openinula/next": "workspace:*",
|
||||||
"@iandx/easy-css": "^0.10.14",
|
"@iandx/easy-css": "^0.10.14",
|
||||||
"babel-preset-inula-next": "workspace:*"
|
"@openinula/babel-preset-inula-next": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9",
|
||||||
"vite-plugin-inula-next": "workspace:*"
|
"@openinula/vite-plugin-inula-next": "workspace:*"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"dlight.js"
|
"inula-next"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,31 +74,39 @@ function Button({ id, text, fn }) {
|
||||||
function App() {
|
function App() {
|
||||||
let data = [];
|
let data = [];
|
||||||
let selected = null;
|
let selected = null;
|
||||||
|
|
||||||
function run() {
|
function run() {
|
||||||
data = buildData(1000);
|
data = buildData(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function runLots() {
|
function runLots() {
|
||||||
data = buildData(10000);
|
data = buildData(10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function add() {
|
function add() {
|
||||||
data.push(...buildData(1000));
|
data.push(...buildData(1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
for (let i = 0; i < data.length; i += 10) {
|
for (let i = 0; i < data.length; i += 10) {
|
||||||
data[i].label += ' !!!';
|
data[i].label += ' !!!';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function swapRows() {
|
function swapRows() {
|
||||||
if (data.length > 998) {
|
if (data.length > 998) {
|
||||||
[data[1], data[998]] = [data[998], data[1]];
|
[data[1], data[998]] = [data[998], data[1]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
data = [];
|
data = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove(id) {
|
function remove(id) {
|
||||||
data = data.filter(d => d.id !== id);
|
data = data.filter(d => d.id !== id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function select(id) {
|
function select(id) {
|
||||||
selected = id;
|
selected = id;
|
||||||
}
|
}
|
||||||
|
@ -124,19 +132,21 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover table-striped test-data">
|
<table class="table table-hover table-striped test-data">
|
||||||
<tbody>
|
<tbody>
|
||||||
<for array={data} item={{ id, label }} key={id}>
|
<for each={data}>
|
||||||
<tr class={selected === id ? 'danger' : ''}>
|
{({ id, label }) => (
|
||||||
<td class="col-md-1" textContent={id} />
|
<tr className={selected === id ? 'danger' : ''}>
|
||||||
<td class="col-md-4">
|
<td className="col-md-1" textContent={id} />
|
||||||
<a onClick={select.bind(this, id)} textContent={label} />
|
<td className="col-md-4">
|
||||||
</td>
|
<a onClick={select.bind(this, id)} textContent={label} />
|
||||||
<td class="col-md-1">
|
</td>
|
||||||
<a onClick={remove.bind(this, id)}>
|
<td className="col-md-1">
|
||||||
<span class="glyphicon glyphicon-remove" aria-hidden="true" />
|
<a onClick={remove.bind(this, id)}>
|
||||||
</a>
|
<span className="glyphicon glyphicon-remove" aria-hidden="true" />
|
||||||
</td>
|
</a>
|
||||||
<td class="col-md-6" />
|
</td>
|
||||||
</tr>
|
<td className="col-md-6" />
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</for>
|
</for>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import inula from 'vite-plugin-inula-next';
|
import inula from '@openinula/vite-plugin-inula-next';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
|
|
|
@ -0,0 +1,417 @@
|
||||||
|
@charset "utf-8";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
font-size: 100%;
|
||||||
|
vertical-align: baseline;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
color: inherit;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.4em;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #111111;
|
||||||
|
min-width: 230px;
|
||||||
|
max-width: 550px;
|
||||||
|
margin: 0 auto;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp {
|
||||||
|
background: #fff;
|
||||||
|
margin: 130px 0 40px 0;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
|
||||||
|
0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp input::-webkit-input-placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp input::-moz-placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp input::input-placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp h1 {
|
||||||
|
position: absolute;
|
||||||
|
top: -140px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 80px;
|
||||||
|
font-weight: 200;
|
||||||
|
text-align: center;
|
||||||
|
color: #b83f45;
|
||||||
|
-webkit-text-rendering: optimizeLegibility;
|
||||||
|
-moz-text-rendering: optimizeLegibility;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-todo,
|
||||||
|
.edit {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: 1.4em;
|
||||||
|
color: inherit;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid #999;
|
||||||
|
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-todo {
|
||||||
|
padding: 16px 16px 16px 60px;
|
||||||
|
height: 65px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(0, 0, 0, 0.003);
|
||||||
|
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
border-top: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
border: none; /* Mobile Safari */
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 100%;
|
||||||
|
bottom: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all + label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 45px;
|
||||||
|
height: 65px;
|
||||||
|
font-size: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: -65px;
|
||||||
|
left: -0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all + label:before {
|
||||||
|
content: '❯';
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 22px;
|
||||||
|
color: #949494;
|
||||||
|
padding: 10px 27px 10px 27px;
|
||||||
|
-webkit-transform: rotate(90deg);
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all:checked + label:before {
|
||||||
|
color: #484848;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li {
|
||||||
|
position: relative;
|
||||||
|
font-size: 24px;
|
||||||
|
border-bottom: 1px solid #ededed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li.editing {
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li.editing .edit {
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 43px);
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 0 0 0 43px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li.editing .view {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .toggle {
|
||||||
|
text-align: center;
|
||||||
|
width: 40px;
|
||||||
|
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||||
|
height: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto 0;
|
||||||
|
border: none; /* Mobile Safari */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .toggle {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .toggle + label {
|
||||||
|
/*
|
||||||
|
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
|
||||||
|
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
|
||||||
|
*/
|
||||||
|
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .toggle:checked + label {
|
||||||
|
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E');
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li label {
|
||||||
|
word-break: break-all;
|
||||||
|
padding: 15px 15px 15px 60px;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.2;
|
||||||
|
transition: color 0.4s;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #484848;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li.completed label {
|
||||||
|
color: #949494;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .destroy {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin: auto 0;
|
||||||
|
font-size: 30px;
|
||||||
|
color: #949494;
|
||||||
|
transition: color 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .destroy:hover,
|
||||||
|
.todo-list li .destroy:focus {
|
||||||
|
color: #C18585;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .destroy:after {
|
||||||
|
content: '×';
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li:hover .destroy {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .edit {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li.editing:last-child {
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 10px 15px;
|
||||||
|
height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 15px;
|
||||||
|
border-top: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
|
||||||
|
0 8px 0 -3px #f6f6f6,
|
||||||
|
0 9px 1px -3px rgba(0, 0, 0, 0.2),
|
||||||
|
0 16px 0 -6px #f6f6f6,
|
||||||
|
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-count {
|
||||||
|
float: left;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-count strong {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters li a {
|
||||||
|
color: inherit;
|
||||||
|
margin: 3px;
|
||||||
|
padding: 3px 7px;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters li a:hover {
|
||||||
|
border-color: #DB7676;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters li a.selected {
|
||||||
|
border-color: #CE4646;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-completed,
|
||||||
|
html .clear-completed:active {
|
||||||
|
float: right;
|
||||||
|
position: relative;
|
||||||
|
line-height: 19px;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-completed:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
margin: 65px auto 0;
|
||||||
|
color: #4d4d4d;
|
||||||
|
font-size: 11px;
|
||||||
|
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info p {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Hack to remove background from Mobile Safari.
|
||||||
|
Can't use it globally since it destroys checkboxes in Firefox
|
||||||
|
*/
|
||||||
|
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||||
|
.toggle-all,
|
||||||
|
.todo-list li .toggle {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .toggle {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
.footer {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus,
|
||||||
|
.toggle:focus + label,
|
||||||
|
.toggle-all:focus + label {
|
||||||
|
box-shadow: 0 0 2px 2px #CF7D7D;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.visually-hidden {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
height: 1px;
|
||||||
|
width: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all {
|
||||||
|
width: 40px !important;
|
||||||
|
height: 60px !important;
|
||||||
|
right: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all-label {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
import { render } from '@openinula/next';
|
||||||
|
import { Header } from './components/header';
|
||||||
|
import { Main } from './components/main';
|
||||||
|
import {
|
||||||
|
ADD_ITEM,
|
||||||
|
UPDATE_ITEM,
|
||||||
|
REMOVE_ITEM,
|
||||||
|
TOGGLE_ITEM,
|
||||||
|
REMOVE_ALL_ITEMS,
|
||||||
|
TOGGLE_ALL,
|
||||||
|
REMOVE_COMPLETED_ITEMS,
|
||||||
|
} from './constants';
|
||||||
|
import './app.css';
|
||||||
|
import { Footer } from './components/footer.jsx';
|
||||||
|
// This alphabet uses `A-Za-z0-9_-` symbols.
|
||||||
|
// The order of characters is optimized for better gzip and brotli compression.
|
||||||
|
// References to the same file (works both for gzip and brotli):
|
||||||
|
// `'use`, `andom`, and `rict'`
|
||||||
|
// References to the brotli default dictionary:
|
||||||
|
// `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf`
|
||||||
|
let urlAlphabet = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
|
||||||
|
|
||||||
|
function nanoid(size = 21) {
|
||||||
|
let id = '';
|
||||||
|
// A compact alternative for `for (var i = 0; i < step; i++)`.
|
||||||
|
let i = size;
|
||||||
|
while (i--) {
|
||||||
|
// `| 0` is more compact and faster than `Math.floor()`.
|
||||||
|
id += urlAlphabet[(Math.random() * 64) | 0];
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
let todos = [];
|
||||||
|
const todoReducer = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ADD_ITEM:
|
||||||
|
return state.concat({ id: nanoid(), title: action.payload.title, completed: false });
|
||||||
|
case UPDATE_ITEM:
|
||||||
|
return state.map(todo => (todo.id === action.payload.id ? { ...todo, title: action.payload.title } : todo));
|
||||||
|
case REMOVE_ITEM:
|
||||||
|
return state.filter(todo => todo.id !== action.payload.id);
|
||||||
|
case TOGGLE_ITEM:
|
||||||
|
return state.map(todo => (todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo));
|
||||||
|
case REMOVE_ALL_ITEMS:
|
||||||
|
return [];
|
||||||
|
case TOGGLE_ALL:
|
||||||
|
return state.map(todo =>
|
||||||
|
todo.completed !== action.payload.completed
|
||||||
|
? {
|
||||||
|
...todo,
|
||||||
|
completed: action.payload.completed,
|
||||||
|
}
|
||||||
|
: todo
|
||||||
|
);
|
||||||
|
case REMOVE_COMPLETED_ITEMS:
|
||||||
|
return state.filter(todo => !todo.completed);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Error(`Unknown action: ${action.type}`);
|
||||||
|
};
|
||||||
|
const dispatch = action => {
|
||||||
|
todos = todoReducer(todos, action);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header dispatch={dispatch} />
|
||||||
|
<Main todos={todos} dispatch={dispatch} />
|
||||||
|
<Footer todos={todos} dispatch={dispatch} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, 'main');
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { REMOVE_COMPLETED_ITEMS } from '../constants';
|
||||||
|
|
||||||
|
export function Footer({ todos, dispatch }) {
|
||||||
|
let route = window.location.hash;
|
||||||
|
window.addEventListener('hashchange', () => {
|
||||||
|
console.log('hashchange', window.location.hash);
|
||||||
|
route = window.location.hash.slice(1);
|
||||||
|
});
|
||||||
|
const activeTodos = todos.filter(todo => !todo.completed);
|
||||||
|
|
||||||
|
const removeCompleted = () => dispatch({ type: REMOVE_COMPLETED_ITEMS });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="footer" data-testid="footer">
|
||||||
|
<span className="todo-count">{`${activeTodos.length} ${activeTodos.length === 1 ? 'item' : 'items'} left!`}</span>
|
||||||
|
<ul className="filters" data-testid="footer-navigation">
|
||||||
|
<li>
|
||||||
|
<a className={route === '/' ? 'selected' : ''} href="#/">
|
||||||
|
All
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a className={route === '/active' ? 'selected' : ''} href="#/active">
|
||||||
|
Active
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a className={route === '/completed' ? 'selected' : ''} href="#/completed">
|
||||||
|
Completed
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button className="clear-completed" disabled={activeTodos.length === todos.length} onClick={removeCompleted}>
|
||||||
|
Clear completed
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Input } from './input';
|
||||||
|
import { ADD_ITEM } from '../constants';
|
||||||
|
|
||||||
|
export function Header({ dispatch }) {
|
||||||
|
const addItem = title => dispatch({ type: ADD_ITEM, payload: { title } });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="header" data-testid="header">
|
||||||
|
<h1>todos</h1>
|
||||||
|
<Input onSubmit={addItem} label="New Todo Input" placeholder="What needs to be done?" />
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
const sanitize = string => {
|
||||||
|
const map = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
'/': '/',
|
||||||
|
};
|
||||||
|
const reg = /[&<>"'/]/gi;
|
||||||
|
return string.replace(reg, match => map[match]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasValidMin = (value, min) => {
|
||||||
|
return value.length >= min;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Input({ onSubmit, placeholder, label, defaultValue, onBlur }) {
|
||||||
|
const handleBlur = () => {
|
||||||
|
if (onBlur) onBlur();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const value = e.target.value.trim();
|
||||||
|
|
||||||
|
if (!hasValidMin(value, 2)) return;
|
||||||
|
|
||||||
|
onSubmit(sanitize(value));
|
||||||
|
e.target.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="input-container">
|
||||||
|
<input
|
||||||
|
className="new-todo"
|
||||||
|
id="todo-input"
|
||||||
|
type="text"
|
||||||
|
data-testid="text-input"
|
||||||
|
autoFocus
|
||||||
|
placeholder={placeholder}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
<label className="visually-hidden" htmlFor="todo-input">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { Input } from './input';
|
||||||
|
|
||||||
|
import { TOGGLE_ITEM, REMOVE_ITEM, UPDATE_ITEM } from '../constants';
|
||||||
|
|
||||||
|
export const Item = function Item({ todo, dispatch }) {
|
||||||
|
let isWritable = false;
|
||||||
|
const { title, completed, id } = todo;
|
||||||
|
|
||||||
|
const toggleItem = () => dispatch({ type: TOGGLE_ITEM, payload: { id } });
|
||||||
|
const removeItem = () => dispatch({ type: REMOVE_ITEM, payload: { id } });
|
||||||
|
const updateItem = (id, title) => dispatch({ type: UPDATE_ITEM, payload: { id, title } });
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
isWritable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
isWritable = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = title => {
|
||||||
|
if (title.length === 0) removeItem(id);
|
||||||
|
else updateItem(id, title);
|
||||||
|
|
||||||
|
isWritable = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={todo.completed ? 'completed' : ''} data-testid="todo-item">
|
||||||
|
<div className="view">
|
||||||
|
<if cond={isWritable}>
|
||||||
|
<Input onSubmit={handleUpdate} label="Edit Todo Input" defaultValue={title} onBlur={handleBlur} />
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<input
|
||||||
|
className="toggle"
|
||||||
|
type="checkbox"
|
||||||
|
data-testid="todo-item-toggle"
|
||||||
|
checked={completed}
|
||||||
|
onChange={toggleItem}
|
||||||
|
/>
|
||||||
|
<label data-testid="todo-item-label" onDoubleClick={handleDoubleClick}>
|
||||||
|
{title}
|
||||||
|
</label>
|
||||||
|
<button className="destroy" data-testid="todo-item-button" onClick={removeItem} />
|
||||||
|
</else>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { TOGGLE_ALL } from '../constants';
|
||||||
|
import { Item } from './item.jsx';
|
||||||
|
|
||||||
|
export function Main({ todos, dispatch }) {
|
||||||
|
// listen for route changes
|
||||||
|
let route = window.location.hash;
|
||||||
|
window.addEventListener('hashchange', () => {
|
||||||
|
console.log('hashchange', window.location.hash);
|
||||||
|
route = window.location.hash.slice(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleTodos = todos.filter(todo => {
|
||||||
|
if (route === '/active') return !todo.completed;
|
||||||
|
|
||||||
|
if (route === '/completed') return todo.completed;
|
||||||
|
|
||||||
|
return todo;
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleAll = e => dispatch({ type: TOGGLE_ALL, payload: { completed: e.target.checked } });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="main" data-testid="main">
|
||||||
|
<if cond={visibleTodos.length > 0}>
|
||||||
|
<div className="toggle-all-container">
|
||||||
|
<input
|
||||||
|
className="toggle-all"
|
||||||
|
type="checkbox"
|
||||||
|
data-testid="toggle-all"
|
||||||
|
checked={visibleTodos.every(todo => todo.completed)}
|
||||||
|
onChange={toggleAll}
|
||||||
|
/>
|
||||||
|
<label className="toggle-all-label" htmlFor="toggle-all">
|
||||||
|
Toggle All Input
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</if>
|
||||||
|
<ul className="todo-list" data-testid="todo-list">
|
||||||
|
<for each={visibleTodos}>{todo => <Item todo={todo} key={todo.id} dispatch={dispatch} />}</for>
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const ADD_ITEM = 'ADD_ITEM';
|
||||||
|
export const UPDATE_ITEM = 'UPDATE_ITEM';
|
||||||
|
export const REMOVE_ITEM = 'REMOVE_ITEM';
|
||||||
|
export const TOGGLE_ITEM = 'TOGGLE_ITEM';
|
||||||
|
export const REMOVE_ALL_ITEMS = 'REMOVE_ALL_ITEMS';
|
||||||
|
export const TOGGLE_ALL = 'TOGGLE_ALL';
|
||||||
|
export const REMOVE_COMPLETED_ITEMS = 'REMOVE_COMPLETED_ITEMS';
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<title>Inula-Next</title>
|
||||||
|
<link rel="stylesheet" href="./app.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main" class="todoapp"></div>
|
||||||
|
<script type="module" src="./app.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "inula-next-todo-mvc",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.2",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/standalone": "^7.22.4",
|
||||||
|
"@iandx/easy-css": "^0.10.14",
|
||||||
|
"@openinula/next": "workspace:*",
|
||||||
|
"@openinula/babel-preset-inula-next": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^4.4.9",
|
||||||
|
"@openinula/vite-plugin-inula-next": "workspace:*"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"inula-next"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "preserve",
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"experimentalDecorators": true
|
||||||
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"esm": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import inula from '@openinula/vite-plugin-inula-next';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
minify: false, // 设置为 false 可以关闭代码压缩
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 4320,
|
||||||
|
},
|
||||||
|
base: '',
|
||||||
|
optimizeDeps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
plugins: [inula({ files: '**/*.{ts,js,tsx,jsx}' })],
|
||||||
|
});
|
|
@ -2,11 +2,11 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<title>Dlight.JS</title>
|
<title>Inula-Next</title>
|
||||||
<link rel="stylesheet" href="/src/App.css"/>
|
<link rel="stylesheet" href="/src/App.css"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="main"></div>
|
<div id="main" class="todoapp"></div>
|
||||||
<script type="module" src="/src/App.tsx"></script>
|
<script type="module" src="/src/App.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "dev",
|
"name": "dev",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
@ -10,16 +10,15 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/standalone": "^7.22.4",
|
"@babel/standalone": "^7.22.4",
|
||||||
"@openinula/next": "workspace:*",
|
"@openinula/next": "0.0.2",
|
||||||
"@iandx/easy-css": "^0.10.14",
|
"@openinula/vite-plugin-inula-next": "0.0.4",
|
||||||
"babel-preset-inula-next": "workspace:*"
|
"classnames": "^2.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9"
|
||||||
"vite-plugin-inula-next": "workspace:*"
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"dlight.js"
|
"inula-next"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,81 @@
|
||||||
.ok {
|
/* tree-view.css */
|
||||||
color: var(--color-ok);
|
|
||||||
}
|
.app {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-title {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-view {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-view > .tree-node:first-child {
|
||||||
|
margin-top: 5px; /* Add margin to the first top-level tree-node */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-children > .tree-node:first-child {
|
||||||
|
margin-top: 5px; /* Add margin to the first top-level tree-node */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tree-node-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-content:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-toggle {
|
||||||
|
min-width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-toggle.expanded {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-toggle.collapsed {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-name {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-children {
|
||||||
|
margin-left: 16px;
|
||||||
|
padding-left: 16px;
|
||||||
|
border-left: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add some color to differentiate levels */
|
||||||
|
.tree-node .tree-node .tree-node-content {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node .tree-node .tree-node .tree-node-content {
|
||||||
|
background-color: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
|
@ -1,17 +1,95 @@
|
||||||
import { View, render } from '@openinula/next';
|
import { render } from '@openinula/next';
|
||||||
|
|
||||||
function MyComp() {
|
const TreeNode = ({ node, onToggle }) => {
|
||||||
let count = 0;
|
let isExpanded = true;
|
||||||
const db = count * 2;
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
isExpanded = !isExpanded;
|
||||||
|
onToggle(node.id, !isExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="tree-node">
|
||||||
<h1 className="123">Hello dlight fn comp</h1>
|
<div onClick={handleToggle} className="tree-node-content">
|
||||||
<section>
|
<if cond={node.children}>
|
||||||
count: {count}, double is: {db}
|
<span className={`tree-node-toggle ${isExpanded ? 'expanded' : 'collapsed'}`}>▼</span>
|
||||||
<button onClick={() => (count += 1)}>Add</button>
|
</if>
|
||||||
</section>
|
<else>
|
||||||
</>
|
<span className="tree-node-toggle"></span>
|
||||||
|
</else>
|
||||||
|
<span className="tree-node-name">{node.name}</span>
|
||||||
|
</div>
|
||||||
|
<if cond={isExpanded}>
|
||||||
|
<div className="tree-node-children">
|
||||||
|
<if cond={node.children}>
|
||||||
|
<for each={node.children}>
|
||||||
|
{child => {
|
||||||
|
return <TreeNode key={child.id} node={child} onToggle={onToggle} />;
|
||||||
|
}}
|
||||||
|
</for>
|
||||||
|
</if>
|
||||||
|
</div>
|
||||||
|
</if>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TreeView = ({ data }) => {
|
||||||
|
let expandedNodes = [];
|
||||||
|
|
||||||
|
const handleToggle = (nodeId, isExpanded) => {
|
||||||
|
if (isExpanded) {
|
||||||
|
expandedNodes = [...expandedNodes, nodeId];
|
||||||
|
} else {
|
||||||
|
expandedNodes = expandedNodes.filter(id => id !== nodeId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tree-view">
|
||||||
|
<for each={data}>{node => <TreeNode key={node.id} node={node} onToggle={handleToggle} />}</for>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const arr = [
|
||||||
|
{
|
||||||
|
name: '我的文档',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: '工作',
|
||||||
|
children: [
|
||||||
|
{ name: '项目方案.docx' },
|
||||||
|
{ name: '会议记录.txt' },
|
||||||
|
{
|
||||||
|
name: '财务报表',
|
||||||
|
children: [{ name: '2023年第一季度.xlsx' }, { name: '2023年第二季度.xlsx' }, { name: '年度总结.pptx' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '个人',
|
||||||
|
children: [
|
||||||
|
{ name: '简历.pdf' },
|
||||||
|
{ name: '家庭照片.jpg' },
|
||||||
|
{
|
||||||
|
name: '旅行计划',
|
||||||
|
children: [{ name: '暑假海岛游.md' }, { name: '冬季滑雪之旅.md' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: '待办事项清单.txt' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<h1 className="app-title">Tree View</h1>
|
||||||
|
<TreeView data={arr} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
render(MyComp, 'main');
|
|
||||||
|
render(App, document.getElementById('main'));
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from '@openinula/next';
|
||||||
|
function AttributeBinding() {
|
||||||
|
let color = 'green';
|
||||||
|
function changeColor() {
|
||||||
|
color = color === 'red' ? 'green' : 'red';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h1 style={{ color }} onClick={changeColor}>
|
||||||
|
Click me to change color.
|
||||||
|
</h1>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(AttributeBinding, document.getElementById('app'));
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from '@openinula/next';
|
||||||
|
|
||||||
|
function TrafficLight() {
|
||||||
|
let lightIndex = 0;
|
||||||
|
const TRAFFIC_LIGHTS = ['red', 'green'];
|
||||||
|
let light = TRAFFIC_LIGHTS[lightIndex];
|
||||||
|
|
||||||
|
function nextLight() {
|
||||||
|
lightIndex = (lightIndex + 1) % 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={nextLight}>Next light</button>
|
||||||
|
<p>Light is: {light}</p>
|
||||||
|
<p>
|
||||||
|
You must
|
||||||
|
<if cond={light === 'red'}>
|
||||||
|
<span>STOP</span>
|
||||||
|
</if>
|
||||||
|
<else-if cond={light === 'green'}>
|
||||||
|
<span>GO</span>
|
||||||
|
</else-if>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TrafficLight, document.getElementById('app'));
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from '@openinula/next';
|
||||||
|
function Form() {
|
||||||
|
let text = 'a';
|
||||||
|
let checked = true;
|
||||||
|
let picked = 'One';
|
||||||
|
let selected = 'A';
|
||||||
|
let multiSelected = [];
|
||||||
|
function updateValue(e) {
|
||||||
|
text = e.target.value;
|
||||||
|
}
|
||||||
|
function handleCheckboxChange(e) {
|
||||||
|
checked = e.target.checked;
|
||||||
|
}
|
||||||
|
function handleRadioChange(e) {
|
||||||
|
picked = e.target.value;
|
||||||
|
}
|
||||||
|
function handleSelectChange(e) {
|
||||||
|
selected = e.target.value;
|
||||||
|
}
|
||||||
|
function handleMultiSelectChange(e) {
|
||||||
|
multiSelected = Array.from(e.target.selectedOptions).map(option => option.value);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>Text Input</h2>
|
||||||
|
<input value={text} onInput={updateValue} />
|
||||||
|
<p>{text}</p>
|
||||||
|
|
||||||
|
<h2>Checkbox</h2>
|
||||||
|
<input id="checkbox" type="checkbox" checked={checked} onChange={handleCheckboxChange} />
|
||||||
|
<label for="checkbox"> Checked: {checked + ''}</label>
|
||||||
|
|
||||||
|
<h2>Radio</h2>
|
||||||
|
<input type="radio" id="one" value="One" name="num" checked={picked === 'One'} onChange={handleRadioChange} />
|
||||||
|
<label for="one">One</label>
|
||||||
|
<br />
|
||||||
|
<input type="radio" id="two" value="Two" name="num" checked={picked === 'Two'} onChange={handleRadioChange} />
|
||||||
|
<label for="two">Two</label>
|
||||||
|
<p>Picked: {picked}</p>
|
||||||
|
|
||||||
|
<h2>Select</h2>
|
||||||
|
<select id="select" value={selected} onChange={handleSelectChange} style={{ width: '100px' }}>
|
||||||
|
<option value="A">A</option>
|
||||||
|
<option value="B">B</option>
|
||||||
|
<option value="C">C</option>
|
||||||
|
</select>
|
||||||
|
<p>Selected: {selected}</p>
|
||||||
|
|
||||||
|
<h2>Multi Select</h2>
|
||||||
|
<select multiple style={{ width: '100px' }} value={multiSelected} onChange={handleMultiSelectChange}>
|
||||||
|
<option value="A">A</option>
|
||||||
|
<option value="B">B</option>
|
||||||
|
<option value="C">C</option>
|
||||||
|
</select>
|
||||||
|
<p>Selected: {multiSelected}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(Form, document.getElementById('app'));
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from '@openinula/next';
|
||||||
|
|
||||||
|
const API_URL = 'https://jsonplaceholder.typicode.com/users';
|
||||||
|
const users = await (await fetch(API_URL)).json();
|
||||||
|
function List({ arr }) {
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
<for each={arr}>{item => <li>{item.name}</li>}</for>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <List arr={users} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, document.getElementById('app'));
|
|
@ -13,12 +13,9 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import plugin from '../dist';
|
import { render } from '@openinula/next';
|
||||||
import { transform as transformWithBabel } from '@babel/core';
|
function HelloWorld() {
|
||||||
|
return <h1>Hello World!</h1>;
|
||||||
export function transform(code: string) {
|
|
||||||
return transformWithBabel(code, {
|
|
||||||
presets: [plugin],
|
|
||||||
filename: 'test.tsx',
|
|
||||||
})?.code;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render(HelloWorld, document.getElementById('app'));
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from '@openinula/next';
|
||||||
|
|
||||||
|
function Colors() {
|
||||||
|
const colors = ['red', 'green', 'blue'];
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
<for each={colors}>{color => <li>{color}</li>}</for>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(Colors, document.getElementById('app'));
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from '@openinula/next';
|
||||||
|
|
||||||
|
let fruits = ['apple', 'banana', 'pear'];
|
||||||
|
function List({ arr }) {
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
<for each={arr}>{item => <li>{item}</li>}</for>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <List arr={fruits} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, document.getElementById('app'));
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from '@openinula/next';
|
||||||
|
function UserInput() {
|
||||||
|
let count = 0;
|
||||||
|
function incrementCount() {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>{count}</h1>
|
||||||
|
<button onClick={incrementCount}>Add 1</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(UserInput, document.getElementById('app'));
|
|
@ -1,7 +1,10 @@
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import inula from 'vite-plugin-inula-next';
|
import inula from '@openinula/vite-plugin-inula-next';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
minify: false, // 设置为 false 可以关闭代码压缩
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 4320,
|
port: 4320,
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"name": "@openinula/next-shared",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Inula Next Shared",
|
||||||
|
"keywords": [
|
||||||
|
"Inula-Next"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"files": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup --sourcemap"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.3.2",
|
||||||
|
"tsup": "^8.2.4"
|
||||||
|
},
|
||||||
|
"tsup": {
|
||||||
|
"entry": [
|
||||||
|
"src/index.ts"
|
||||||
|
],
|
||||||
|
"format": [
|
||||||
|
"cjs",
|
||||||
|
"esm"
|
||||||
|
],
|
||||||
|
"clean": true,
|
||||||
|
"dts": true,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
export enum InulaNodeType {
|
||||||
|
Comp = 0,
|
||||||
|
For = 1,
|
||||||
|
Cond = 2,
|
||||||
|
Exp = 3,
|
||||||
|
Hook = 4,
|
||||||
|
Context = 5,
|
||||||
|
Children = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTypeName(type: InulaNodeType): string {
|
||||||
|
return InulaNodeType[type];
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"esm": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "@openinula/store",
|
"name": "@openinula/store",
|
||||||
"version": "0.0.0",
|
"version": "0.0.1",
|
||||||
"description": "DLight shared store",
|
"description": "Inula shared store",
|
||||||
"author": {
|
|
||||||
"name": "IanDx",
|
|
||||||
"email": "iandxssxx@gmail.com"
|
|
||||||
},
|
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"dlight.js"
|
"Inula-Next"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
"files": [
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
# DLight Main Package
|
# Inula-Next Main Package
|
||||||
See the website's documentations for usage.
|
See the website's documentations for usage.
|
||||||
|
|
|
@ -1,44 +1,41 @@
|
||||||
{
|
{
|
||||||
"name": "@openinula/next",
|
"name": "@openinula/next",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"author": {
|
"keywords": [
|
||||||
"name": "IanDx",
|
"inula"
|
||||||
"email": "iandxssxx@gmail.com"
|
],
|
||||||
},
|
"license": "MIT",
|
||||||
"keywords": [
|
"files": [
|
||||||
"inula"
|
"dist",
|
||||||
],
|
"README.md"
|
||||||
"license": "MIT",
|
],
|
||||||
"files": [
|
"type": "module",
|
||||||
"dist",
|
"main": "dist/index.cjs",
|
||||||
"README.md"
|
"module": "dist/index.js",
|
||||||
],
|
"typings": "dist/index.d.ts",
|
||||||
"type": "module",
|
"scripts": {
|
||||||
"main": "dist/index.cjs",
|
"build": "tsup --sourcemap",
|
||||||
"module": "dist/index.js",
|
"test": "vitest --ui"
|
||||||
"typings": "dist/index.d.ts",
|
},
|
||||||
"scripts": {
|
"dependencies": {
|
||||||
"build": "tsup --sourcemap && cp src/index.d.ts dist/ && cp -r src/types dist/",
|
"csstype": "^3.1.3",
|
||||||
"test": "vitest --ui"
|
"@openinula/next-shared": "workspace:*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
"csstype": "^3.1.3",
|
"tsup": "^6.5.0",
|
||||||
"@openinula/store": "workspace:*"
|
"@openinula/vite-plugin-inula-next": "workspace:*",
|
||||||
},
|
"vitest": "2.0.5"
|
||||||
"devDependencies": {
|
},
|
||||||
"tsup": "^6.5.0",
|
"tsup": {
|
||||||
"vite-plugin-inula-next": "workspace:*",
|
"entry": [
|
||||||
"vitest": "^1.2.2"
|
"src/index.ts"
|
||||||
},
|
],
|
||||||
"tsup": {
|
"format": [
|
||||||
"entry": [
|
"cjs",
|
||||||
"src/index.js"
|
"esm"
|
||||||
],
|
],
|
||||||
"format": [
|
"clean": true,
|
||||||
"cjs",
|
"minify": false,
|
||||||
"esm"
|
"noExternal": ["@openinula/next-shared"]
|
||||||
],
|
}
|
||||||
"clean": true,
|
}
|
||||||
"minify": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { addWillUnmount } from './lifecycle';
|
||||||
|
import { ChildrenNode, VNode, Updater } from './types';
|
||||||
|
import { InulaNodeType } from '@openinula/next-shared';
|
||||||
|
|
||||||
|
export function createChildrenNode(childrenFunc: (addUpdate: (updater: Updater) => void) => VNode[]): ChildrenNode {
|
||||||
|
return {
|
||||||
|
__type: InulaNodeType.Children,
|
||||||
|
childrenFunc,
|
||||||
|
updaters: new Set(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Build the prop view by calling the childrenFunc and add every single instance of the returned InulaNode to updaters
|
||||||
|
* @returns An array of InulaNode instances returned by childrenFunc
|
||||||
|
*/
|
||||||
|
export function buildChildren(childrenNode: ChildrenNode) {
|
||||||
|
let update;
|
||||||
|
const addUpdate = (updateFunc: Updater) => {
|
||||||
|
update = updateFunc;
|
||||||
|
childrenNode.updaters.add(updateFunc);
|
||||||
|
};
|
||||||
|
const newNodes = childrenNode.childrenFunc(addUpdate);
|
||||||
|
if (newNodes.length === 0) return [];
|
||||||
|
if (update) {
|
||||||
|
// Remove the updateNode from dlUpdateNodes when it unmounts
|
||||||
|
addWillUnmount(newNodes[0], childrenNode.updaters.delete.bind(childrenNode.updaters, update));
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update every node in dlUpdateNodes
|
||||||
|
* @param changed - A parameter indicating what changed to trigger the update
|
||||||
|
*/
|
||||||
|
export function updateChildrenNode(childrenNode: ChildrenNode, changed: number) {
|
||||||
|
childrenNode.updaters.forEach(update => {
|
||||||
|
update(changed);
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,373 +0,0 @@
|
||||||
import { DLNode, DLNodeType } from './DLNode';
|
|
||||||
import { forwardHTMLProp } from './HTMLNode';
|
|
||||||
import { DLStore, cached } from './store';
|
|
||||||
import { schedule } from './scheduler';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class
|
|
||||||
* @extends import('./DLNode').DLNode
|
|
||||||
*/
|
|
||||||
export class CompNode extends DLNode {
|
|
||||||
/**
|
|
||||||
* @brief Constructor, Comp type
|
|
||||||
* @internal
|
|
||||||
* * key - private property key
|
|
||||||
* * $$key - dependency number, e.g. 0b1, 0b10, 0b100
|
|
||||||
* * $s$key - set of properties that depend on this property
|
|
||||||
* * $p$key - exist if this property is a prop
|
|
||||||
* * $e$key - exist if this property is an env
|
|
||||||
* * $en$key - exist if this property is an env, and it's the innermost env that contains this env
|
|
||||||
* * $w$key - exist if this property is a watcher
|
|
||||||
* * $f$key - a function that returns the value of this property, called when the property's dependencies change
|
|
||||||
* * _$children - children nodes of type PropView
|
|
||||||
* * _$contentKey - the key key of the content prop
|
|
||||||
* * _$forwardProps - exist if this node is forwarding props
|
|
||||||
* * _$forwardPropsId - the keys of the props that this node is forwarding, collected in _$setForwardProp
|
|
||||||
* * _$forwardPropsSet - contain all the nodes that are forwarding props to this node, collected with _$addForwardProps
|
|
||||||
*/
|
|
||||||
constructor() {
|
|
||||||
super(DLNodeType.Comp);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Init function, called explicitly in the subclass's constructor
|
|
||||||
* @param props - Object containing properties
|
|
||||||
* @param content - Content to be used
|
|
||||||
* @param children - Child nodes
|
|
||||||
* @param forwardPropsScope - Scope for forwarding properties
|
|
||||||
*/
|
|
||||||
_$init(props, content, children, forwardPropsScope) {
|
|
||||||
this._$notInitd = true;
|
|
||||||
|
|
||||||
// ---- Forward props first to allow internal props to override forwarded props
|
|
||||||
if (forwardPropsScope) forwardPropsScope._$addForwardProps(this);
|
|
||||||
if (content) this._$setContent(() => content[0], content[1]);
|
|
||||||
if (props)
|
|
||||||
props.forEach(([key, value, deps]) => {
|
|
||||||
if (key === 'props') return this._$setProps(() => value, deps);
|
|
||||||
this._$setProp(key, () => value, deps);
|
|
||||||
});
|
|
||||||
if (children) this._$children = children;
|
|
||||||
|
|
||||||
// ---- Add envs
|
|
||||||
DLStore.global.DLEnvStore &&
|
|
||||||
Object.entries(DLStore.global.DLEnvStore.envs).forEach(([key, [value, envNode]]) => {
|
|
||||||
if (key === '_$catchable') {
|
|
||||||
this._$catchable = value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!(`$e$${key}` in this)) return;
|
|
||||||
envNode.addNode(this);
|
|
||||||
this._$initEnv(key, value, envNode);
|
|
||||||
});
|
|
||||||
|
|
||||||
const willCall = () => {
|
|
||||||
this._$callUpdatesBeforeInit();
|
|
||||||
this.didMount && DLNode.addDidMount(this, this.didMount.bind(this));
|
|
||||||
this.willUnmount && DLNode.addWillUnmount(this, this.willUnmount.bind(this));
|
|
||||||
DLNode.addDidUnmount(this, this._$setUnmounted.bind(this));
|
|
||||||
this.didUnmount && DLNode.addDidUnmount(this, this.didUnmount.bind(this));
|
|
||||||
this.willMount?.();
|
|
||||||
this._$nodes = this.Body?.() ?? [];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this._$catchable) {
|
|
||||||
this._$catchable(willCall)();
|
|
||||||
if (this._$update) this._$update = this._$catchable(this._$update.bind(this));
|
|
||||||
this._$updateDerived = this._$catchable(this._$updateDerived.bind(this));
|
|
||||||
delete this._$catchable;
|
|
||||||
} else {
|
|
||||||
willCall();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_$setUnmounted() {
|
|
||||||
this._$unmounted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Call updates manually before the node is mounted
|
|
||||||
*/
|
|
||||||
_$callUpdatesBeforeInit() {
|
|
||||||
const protoProps = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
|
|
||||||
const ownProps = Object.getOwnPropertyNames(this);
|
|
||||||
const allProps = [...protoProps, ...ownProps];
|
|
||||||
allProps.forEach(key => {
|
|
||||||
// ---- Run watcher
|
|
||||||
if (key.startsWith('$w$')) return this[key.slice(3)]();
|
|
||||||
// ---- Run model update
|
|
||||||
if (key.startsWith('$md$')) {
|
|
||||||
const realKey = key.slice(4);
|
|
||||||
this[realKey] = this[realKey]();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ---- Run derived value
|
|
||||||
if (key.startsWith('$f$')) {
|
|
||||||
const realKey = key.slice(3);
|
|
||||||
this[realKey] = this[key];
|
|
||||||
this._$updateDerived(realKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
delete this._$notInitd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set all the props to forward
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
* @param deps
|
|
||||||
*/
|
|
||||||
_$setPropToForward(key, value, deps) {
|
|
||||||
this._$forwardPropsSet.forEach(node => {
|
|
||||||
const isContent = key === '_$content';
|
|
||||||
if (node._$dlNodeType === DLNodeType.Comp) {
|
|
||||||
if (isContent) node._$setContent(() => value, deps);
|
|
||||||
else node._$setProp(key, () => value, deps);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (node instanceof HTMLElement) {
|
|
||||||
if (isContent) key = 'textContent';
|
|
||||||
forwardHTMLProp(node, key, () => value, deps);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Define forward props
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
_$setForwardProp(key, valueFunc, deps) {
|
|
||||||
const notInitd = '_$notInitd' in this;
|
|
||||||
if (!notInitd && this._$cache(key, deps)) return;
|
|
||||||
const value = valueFunc();
|
|
||||||
if (key === '_$content' && this._$contentKey) {
|
|
||||||
this[this._$contentKey] = value;
|
|
||||||
this._$updateDerived(this._$contentKey);
|
|
||||||
}
|
|
||||||
this[key] = value;
|
|
||||||
this._$updateDerived(key);
|
|
||||||
if (notInitd) this._$forwardPropsId.push(key);
|
|
||||||
else this._$setPropToForward(key, value, deps);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add a node to the set of nodes that are forwarding props to this node and init these props
|
|
||||||
* @param node
|
|
||||||
*/
|
|
||||||
_$addForwardProps(node) {
|
|
||||||
this._$forwardPropsSet.add(node);
|
|
||||||
this._$forwardPropsId.forEach(key => {
|
|
||||||
this._$setPropToForward(key, this[key], []);
|
|
||||||
});
|
|
||||||
DLNode.addWillUnmount(node, this._$forwardPropsSet.delete.bind(this._$forwardPropsSet, node));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Cache the deps and return true if the deps are the same as the previous deps
|
|
||||||
* @param key
|
|
||||||
* @param deps
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
_$cache(key, deps) {
|
|
||||||
if (!deps || !deps.length) return false;
|
|
||||||
const cacheKey = `$cc$${key}`;
|
|
||||||
if (cached(deps, this[cacheKey])) return true;
|
|
||||||
this[cacheKey] = deps;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set the content prop, the key is stored in _$contentKey
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
_$setContent(valueFunc, deps) {
|
|
||||||
if ('_$forwardProps' in this) return this._$setForwardProp('_$content', valueFunc, deps);
|
|
||||||
const contentKey = this._$contentKey;
|
|
||||||
if (!contentKey) return;
|
|
||||||
if (this._$cache(contentKey, deps)) return;
|
|
||||||
this[contentKey] = valueFunc();
|
|
||||||
this._$updateDerived(contentKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set a prop directly, if this is a forwarded prop, go and init forwarded props
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
* @param deps
|
|
||||||
*/
|
|
||||||
_$setProp(key, valueFunc, deps) {
|
|
||||||
if ('_$forwardProps' in this) return this._$setForwardProp(key, valueFunc, deps);
|
|
||||||
if (!(`$p$${key}` in this)) {
|
|
||||||
console.warn(`[${key}] is not a prop in ${this.constructor.name}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this._$cache(key, deps)) return;
|
|
||||||
this[key] = valueFunc();
|
|
||||||
this._$updateDerived(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
_$setProps(valueFunc, deps) {
|
|
||||||
if (this._$cache('props', deps)) return;
|
|
||||||
const props = valueFunc();
|
|
||||||
if (!props) return;
|
|
||||||
Object.entries(props).forEach(([key, value]) => {
|
|
||||||
this._$setProp(key, () => value, []);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Init an env, put the corresponding innermost envNode in $en$key
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
* @param envNode
|
|
||||||
*/
|
|
||||||
_$initEnv(key, value, envNode) {
|
|
||||||
this[key] = value;
|
|
||||||
this[`$en$${key}`] = envNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Update functions
|
|
||||||
/**
|
|
||||||
* @brief Update an env, called in EnvNode._$update
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
* @param envNode
|
|
||||||
*/
|
|
||||||
_$updateEnv(key, value, envNode) {
|
|
||||||
if (!(`$e$${key}` in this)) return;
|
|
||||||
if (envNode !== this[`$en$${key}`]) return;
|
|
||||||
this[key] = value;
|
|
||||||
this._$updateDerived(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update a prop
|
|
||||||
*/
|
|
||||||
_$ud(exp, key) {
|
|
||||||
this._$updateDerived(key);
|
|
||||||
return exp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update properties that depend on this property
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
_$updateDerived(key) {
|
|
||||||
if ('_$notInitd' in this) return;
|
|
||||||
|
|
||||||
this[`$s$${key}`]?.forEach(k => {
|
|
||||||
if (`$w$${k}` in this) {
|
|
||||||
// ---- Watcher
|
|
||||||
this[k](key);
|
|
||||||
} else if (`$md$${k}` in this) {
|
|
||||||
this[k]._$update();
|
|
||||||
} else {
|
|
||||||
// ---- Regular derived value
|
|
||||||
this[k] = this[`$f$${k}`];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- "trigger-view"
|
|
||||||
this._$updateView(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
_$updateView(key) {
|
|
||||||
if (this._$modelCallee) return this._$updateModelCallee();
|
|
||||||
if (!('_$update' in this)) return;
|
|
||||||
const depNum = this[`$$${key}`];
|
|
||||||
if (!depNum) return;
|
|
||||||
// ---- Collect all depNums that need to be updated
|
|
||||||
if ('_$depNumsToUpdate' in this) {
|
|
||||||
this._$depNumsToUpdate.push(depNum);
|
|
||||||
} else {
|
|
||||||
this._$depNumsToUpdate = [depNum];
|
|
||||||
// ---- Update in the next microtask
|
|
||||||
schedule(() => {
|
|
||||||
// ---- Abort if unmounted
|
|
||||||
if (this._$unmounted) return;
|
|
||||||
const depNums = this._$depNumsToUpdate;
|
|
||||||
if (depNums.length > 0) {
|
|
||||||
const depNum = depNums.reduce((acc, cur) => acc | cur, 0);
|
|
||||||
this._$update(depNum);
|
|
||||||
}
|
|
||||||
delete this._$depNumsToUpdate;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_$updateModelCallee() {
|
|
||||||
if ('_$depNumsToUpdate' in this) return;
|
|
||||||
this._$depNumsToUpdate = true;
|
|
||||||
// ---- Update in the next microtask
|
|
||||||
Promise.resolve().then(() => {
|
|
||||||
// ---- Abort if unmounted
|
|
||||||
if (this._$unmounted) return;
|
|
||||||
this._$modelCallee._$updateDerived(this._$modelKey);
|
|
||||||
delete this._$depNumsToUpdate;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update all props and content of the model
|
|
||||||
*/
|
|
||||||
static _$updateModel(model, propsFunc, contentFunc) {
|
|
||||||
// ---- Suppress update because top level update will be performed
|
|
||||||
// directly by the state variable in the model callee, which will
|
|
||||||
// trigger the update of the model
|
|
||||||
const props = propsFunc() ?? {};
|
|
||||||
const collectedProps = props.s ?? [];
|
|
||||||
props.m?.forEach(([props, deps]) => {
|
|
||||||
Object.entries(props).forEach(([key, value]) => {
|
|
||||||
collectedProps.push([key, value, deps]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
collectedProps.forEach(([key, value, deps]) => {
|
|
||||||
model._$setProp(key, () => value, deps);
|
|
||||||
});
|
|
||||||
const content = contentFunc();
|
|
||||||
if (content) model._$setContent(() => content[0], content[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
static _$releaseModel() {
|
|
||||||
delete this._$modelCallee;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Inject Dlight model in to a property
|
|
||||||
* @param ModelCls
|
|
||||||
* @param props { m: [props, deps], s: [key, value, deps] }
|
|
||||||
* @param content
|
|
||||||
* @param key
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
_$injectModel(ModelCls, propsFunc, contentFunc, key) {
|
|
||||||
const props = propsFunc() ?? {};
|
|
||||||
const collectedProps = props.s ?? [];
|
|
||||||
props.m?.forEach(([props, deps]) => {
|
|
||||||
Object.entries(props).forEach(([key, value]) => {
|
|
||||||
collectedProps.push([key, value, deps]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const model = new ModelCls();
|
|
||||||
model._$init(collectedProps, contentFunc(), null, null);
|
|
||||||
model._$modelCallee = this;
|
|
||||||
model._$modelKey = key;
|
|
||||||
model._$update = CompNode._$updateModel.bind(null, model, propsFunc, contentFunc);
|
|
||||||
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- @View -> class Comp extends View
|
|
||||||
export const View = CompNode;
|
|
||||||
export const Model = CompNode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Run all update functions given the key
|
|
||||||
* @param dlNode
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
export function update(dlNode, key) {
|
|
||||||
dlNode._$updateDerived(key);
|
|
||||||
}
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { addDidMount, addDidUnmount, addWillUnmount } from './lifecycle';
|
||||||
|
import { equal } from './equal';
|
||||||
|
import { schedule } from './scheduler';
|
||||||
|
import { inMount } from './index';
|
||||||
|
import { CompNode, ComposableNode } from './types';
|
||||||
|
import { InulaNodeType } from '@openinula/next-shared';
|
||||||
|
|
||||||
|
export function createCompNode(): CompNode {
|
||||||
|
return {
|
||||||
|
updateProp: builtinUpdateFunc,
|
||||||
|
updateState: builtinUpdateFunc,
|
||||||
|
__type: InulaNodeType.Comp,
|
||||||
|
props: {},
|
||||||
|
_$nodes: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function builtinUpdateFunc() {
|
||||||
|
throw new Error('Component node not initiated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function constructComp(
|
||||||
|
comp: CompNode,
|
||||||
|
{
|
||||||
|
updateState,
|
||||||
|
updateProp,
|
||||||
|
updateContext,
|
||||||
|
getUpdateViews,
|
||||||
|
didUnmount,
|
||||||
|
willUnmount,
|
||||||
|
didMount,
|
||||||
|
}: Pick<
|
||||||
|
CompNode,
|
||||||
|
'updateState' | 'updateProp' | 'updateContext' | 'getUpdateViews' | 'didUnmount' | 'willUnmount' | 'didMount'
|
||||||
|
>
|
||||||
|
): CompNode {
|
||||||
|
comp.updateState = updateState;
|
||||||
|
comp.updateProp = updateProp;
|
||||||
|
comp.updateContext = updateContext;
|
||||||
|
comp.getUpdateViews = getUpdateViews;
|
||||||
|
comp.didUnmount = didUnmount;
|
||||||
|
comp.willUnmount = willUnmount;
|
||||||
|
comp.didMount = didMount;
|
||||||
|
|
||||||
|
return comp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initCompNode(node: CompNode): CompNode {
|
||||||
|
node.mounting = true;
|
||||||
|
const willCall = () => {
|
||||||
|
callUpdatesBeforeInit(node);
|
||||||
|
if (node.didMount) addDidMount(node, node.didMount);
|
||||||
|
if (node.willUnmount) addWillUnmount(node, node.willUnmount);
|
||||||
|
addDidUnmount(node, setUnmounted.bind(null, node));
|
||||||
|
if (node.didUnmount) addDidUnmount(node, node.didUnmount);
|
||||||
|
if (node.getUpdateViews) {
|
||||||
|
const result = node.getUpdateViews();
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
const [baseNode, updateView] = result;
|
||||||
|
node.updateView = updateView;
|
||||||
|
node._$nodes = baseNode;
|
||||||
|
} else {
|
||||||
|
node.updateView = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
willCall();
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUnmounted(node: CompNode) {
|
||||||
|
node._$unmounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function callUpdatesBeforeInit(node: CompNode) {
|
||||||
|
node.updateState(-1);
|
||||||
|
delete node.mounting;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheCheck(node: CompNode, key: string, deps: any[]): boolean {
|
||||||
|
if (!deps || !deps.length) return false;
|
||||||
|
if (!node.cache) {
|
||||||
|
node.cache = {};
|
||||||
|
}
|
||||||
|
if (equal(deps, node.cache[key])) return true;
|
||||||
|
node.props[key] = deps;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setProp(node: CompNode, key: string, valueFunc: () => any, deps: any[]) {
|
||||||
|
if (cacheCheck(node, key, deps)) return;
|
||||||
|
node.props[key] = valueFunc();
|
||||||
|
node.updateProp(key, node.props[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setProps(node: CompNode, valueFunc: () => Record<string, any>, deps: any[]) {
|
||||||
|
if (cacheCheck(node, 'props', deps)) return;
|
||||||
|
const props = valueFunc();
|
||||||
|
if (!props) return;
|
||||||
|
Object.entries(props).forEach(([key, value]) => {
|
||||||
|
setProp(node, key, () => value, []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateContext(node: CompNode, key: string, value: any, context: any) {
|
||||||
|
if (!node.updateContext) return;
|
||||||
|
node.updateContext(context, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCompNode(node: ComposableNode, newValue: any, bit?: number) {
|
||||||
|
if ('mounting' in node) return;
|
||||||
|
|
||||||
|
node.updateState(bit || 0);
|
||||||
|
|
||||||
|
if (!inMount()) {
|
||||||
|
updateView(node, bit || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateView(node: ComposableNode, bit: number) {
|
||||||
|
if (!bit) return;
|
||||||
|
if ('_$depNumsToUpdate' in node) {
|
||||||
|
node._$depNumsToUpdate?.push(bit);
|
||||||
|
} else {
|
||||||
|
node._$depNumsToUpdate = [bit];
|
||||||
|
schedule(() => {
|
||||||
|
if (node._$unmounted) return;
|
||||||
|
const depNums = node._$depNumsToUpdate || [];
|
||||||
|
if (depNums.length > 0) {
|
||||||
|
const depNum = depNums.reduce((acc, cur) => acc | cur, 0);
|
||||||
|
node.updateView?.(depNum);
|
||||||
|
}
|
||||||
|
delete node._$depNumsToUpdate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
import { InulaNodeType } from '@openinula/next-shared';
|
||||||
|
import { addWillUnmount } from './lifecycle';
|
||||||
|
import { equal } from './equal';
|
||||||
|
import { VNode, ContextNode, Context, CompNode, HookNode } from './types';
|
||||||
|
import { currentComp } from '.';
|
||||||
|
|
||||||
|
let contextNodeMap: Map<symbol, ContextNode<any>>;
|
||||||
|
|
||||||
|
export function getContextNodeMap() {
|
||||||
|
return contextNodeMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createContextNode<V extends Record<PropertyKey, any>>(
|
||||||
|
ctx: Context<V>,
|
||||||
|
value: V,
|
||||||
|
depMap: Record<keyof V, Array<unknown>>
|
||||||
|
) {
|
||||||
|
if (!contextNodeMap) contextNodeMap = new Map();
|
||||||
|
|
||||||
|
const ContextNode: ContextNode<V> = {
|
||||||
|
value: value,
|
||||||
|
depMap: depMap,
|
||||||
|
context: ctx,
|
||||||
|
__type: InulaNodeType.Context,
|
||||||
|
consumers: new Set(),
|
||||||
|
_$nodes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
replaceContextValue(ContextNode);
|
||||||
|
|
||||||
|
return ContextNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update a specific key of context, and update all the comp nodes that depend on this context
|
||||||
|
* @param contextNode
|
||||||
|
* @param name - The name of the environment variable to update
|
||||||
|
* @param valueFunc
|
||||||
|
* @param deps
|
||||||
|
*/
|
||||||
|
export function updateContextNode<V extends Record<string, any>>(
|
||||||
|
contextNode: ContextNode<V>,
|
||||||
|
name: keyof V,
|
||||||
|
valueFunc: () => V[keyof V],
|
||||||
|
deps: Array<V[keyof V]>
|
||||||
|
) {
|
||||||
|
if (cached(contextNode, deps, name)) return;
|
||||||
|
const value = valueFunc();
|
||||||
|
contextNode.value[name] = value;
|
||||||
|
contextNode.consumers.forEach(node => {
|
||||||
|
// should have updateContext, otherwise the bug of compiler
|
||||||
|
node.updateContext!(contextNode.context, name as string, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cached<V extends Record<PropertyKey, any>>(contextNode: ContextNode<V>, deps: Array<unknown>, name: keyof V) {
|
||||||
|
if (!deps || !deps.length) return false;
|
||||||
|
if (equal(deps, contextNode.depMap[name])) return true;
|
||||||
|
contextNode.depMap[name] = deps;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceContextValue<V extends Record<PropertyKey, any>>(contextNode: ContextNode<V>) {
|
||||||
|
contextNode.prevValue = contextNode.context.value;
|
||||||
|
contextNode.prevContextNode = contextNodeMap!.get(contextNode.context.id);
|
||||||
|
contextNode.context.value = contextNode.value;
|
||||||
|
|
||||||
|
contextNodeMap!.set(contextNode.context.id, contextNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set this._$nodes, and exit the current context
|
||||||
|
* @param contextNode
|
||||||
|
* @param nodes - The nodes to set
|
||||||
|
*/
|
||||||
|
export function initContextChildren<V extends Record<PropertyKey, any>>(contextNode: ContextNode<V>, nodes: VNode[]) {
|
||||||
|
contextNode._$nodes = nodes;
|
||||||
|
contextNode.context.value = contextNode.prevValue || null;
|
||||||
|
if (contextNode.prevContextNode) {
|
||||||
|
contextNodeMap!.set(contextNode.context.id, contextNode.prevContextNode);
|
||||||
|
} else {
|
||||||
|
contextNodeMap!.delete(contextNode.context.id);
|
||||||
|
}
|
||||||
|
contextNode.prevValue = null;
|
||||||
|
contextNode.prevContextNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceContext(contextNodeMap: Map<symbol, ContextNode<any>>) {
|
||||||
|
for (const [ctxId, contextNode] of contextNodeMap.entries()) {
|
||||||
|
replaceContextValue(contextNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Add a node to this.updateNodes, delete the node from this.updateNodes when it unmounts
|
||||||
|
*/
|
||||||
|
export function addConsumer(contextNode: ContextNode<any>, node: CompNode | HookNode) {
|
||||||
|
contextNode.consumers.add(node);
|
||||||
|
addWillUnmount(node, contextNode.consumers.delete.bind(contextNode.consumers, node));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createContext<T extends Record<PropertyKey, any> | null>(defaultVal: T): Context<T> {
|
||||||
|
return {
|
||||||
|
id: Symbol('inula-ctx'),
|
||||||
|
value: defaultVal,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useContext<T extends Record<PropertyKey, any> | null>(ctx: Context<T>, key?: keyof T): T | T[keyof T] {
|
||||||
|
if (contextNodeMap) {
|
||||||
|
const contextNode = contextNodeMap.get(ctx.id);
|
||||||
|
if (contextNode) {
|
||||||
|
addConsumer(contextNode, currentComp!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key && ctx.value) {
|
||||||
|
return ctx.value[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.value as T;
|
||||||
|
}
|
|
@ -1,210 +0,0 @@
|
||||||
import { DLStore } from './store';
|
|
||||||
|
|
||||||
export const DLNodeType = {
|
|
||||||
Comp: 0,
|
|
||||||
For: 1,
|
|
||||||
Cond: 2,
|
|
||||||
Env: 3,
|
|
||||||
Exp: 4,
|
|
||||||
Snippet: 5,
|
|
||||||
Try: 6,
|
|
||||||
};
|
|
||||||
|
|
||||||
export class DLNode {
|
|
||||||
/**
|
|
||||||
* @brief Node type: HTML, Text, Custom, For, If, Env, Expression
|
|
||||||
*/
|
|
||||||
_$dlNodeType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Constructor
|
|
||||||
* @param nodeType
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
constructor(nodeType) {
|
|
||||||
this._$dlNodeType = nodeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Node element
|
|
||||||
* Either one real element for HTMLNode and TextNode
|
|
||||||
* Or an array of DLNode for CustomNode, ForNode, IfNode, EnvNode, ExpNode
|
|
||||||
*/
|
|
||||||
get _$el() {
|
|
||||||
return DLNode.toEls(this._$nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Loop all child DLNodes to get all the child elements
|
|
||||||
* @param nodes
|
|
||||||
* @returns HTMLElement[]
|
|
||||||
*/
|
|
||||||
static toEls(nodes) {
|
|
||||||
const els = [];
|
|
||||||
this.loopShallowEls(nodes, el => {
|
|
||||||
els.push(el);
|
|
||||||
});
|
|
||||||
return els;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Loop nodes ----
|
|
||||||
/**
|
|
||||||
* @brief Loop all elements shallowly,
|
|
||||||
* i.e., don't loop the child nodes of dom elements and only call runFunc on dom elements
|
|
||||||
* @param nodes
|
|
||||||
* @param runFunc
|
|
||||||
*/
|
|
||||||
static loopShallowEls(nodes, runFunc) {
|
|
||||||
const stack = [...nodes].reverse();
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const node = stack.pop();
|
|
||||||
if (!('_$dlNodeType' in node)) runFunc(node);
|
|
||||||
else node._$nodes && stack.push(...[...node._$nodes].reverse());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add parentEl to all nodes until the first element
|
|
||||||
* @param nodes
|
|
||||||
* @param parentEl
|
|
||||||
*/
|
|
||||||
static addParentEl(nodes, parentEl) {
|
|
||||||
nodes.forEach(node => {
|
|
||||||
if ('_$dlNodeType' in node) {
|
|
||||||
node._$parentEl = parentEl;
|
|
||||||
node._$nodes && DLNode.addParentEl(node._$nodes, parentEl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Flow index and add child elements ----
|
|
||||||
/**
|
|
||||||
* @brief Get the total count of dom elements before the stop node
|
|
||||||
* @param nodes
|
|
||||||
* @param stopNode
|
|
||||||
* @returns total count of dom elements
|
|
||||||
*/
|
|
||||||
static getFlowIndexFromNodes(nodes, stopNode) {
|
|
||||||
let index = 0;
|
|
||||||
const stack = [...nodes].reverse();
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const node = stack.pop();
|
|
||||||
if (node === stopNode) break;
|
|
||||||
if ('_$dlNodeType' in node) {
|
|
||||||
node._$nodes && stack.push(...[...node._$nodes].reverse());
|
|
||||||
} else {
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Given an array of nodes, append them to the parentEl
|
|
||||||
* 1. If nextSibling is provided, insert the nodes before the nextSibling
|
|
||||||
* 2. If nextSibling is not provided, append the nodes to the parentEl
|
|
||||||
* @param nodes
|
|
||||||
* @param parentEl
|
|
||||||
* @param nextSibling
|
|
||||||
* @returns Added element count
|
|
||||||
*/
|
|
||||||
static appendNodesWithSibling(nodes, parentEl, nextSibling) {
|
|
||||||
if (nextSibling) return this.insertNodesBefore(nodes, parentEl, nextSibling);
|
|
||||||
return this.appendNodes(nodes, parentEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Given an array of nodes, append them to the parentEl using the index
|
|
||||||
* 1. If the index is the same as the length of the parentEl.childNodes, append the nodes to the parentEl
|
|
||||||
* 2. If the index is not the same as the length of the parentEl.childNodes, insert the nodes before the node at the index
|
|
||||||
* @param nodes
|
|
||||||
* @param parentEl
|
|
||||||
* @param index
|
|
||||||
* @param length
|
|
||||||
* @returns Added element count
|
|
||||||
*/
|
|
||||||
static appendNodesWithIndex(nodes, parentEl, index, length) {
|
|
||||||
length = length ?? parentEl.childNodes.length;
|
|
||||||
if (length !== index) return this.insertNodesBefore(nodes, parentEl, parentEl.childNodes[index]);
|
|
||||||
return this.appendNodes(nodes, parentEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Insert nodes before the nextSibling
|
|
||||||
* @param nodes
|
|
||||||
* @param parentEl
|
|
||||||
* @param nextSibling
|
|
||||||
* @returns Added element count
|
|
||||||
*/
|
|
||||||
static insertNodesBefore(nodes, parentEl, nextSibling) {
|
|
||||||
let count = 0;
|
|
||||||
this.loopShallowEls(nodes, el => {
|
|
||||||
parentEl.insertBefore(el, nextSibling);
|
|
||||||
count++;
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Append nodes to the parentEl
|
|
||||||
* @param nodes
|
|
||||||
* @param parentEl
|
|
||||||
* @returns Added element count
|
|
||||||
*/
|
|
||||||
static appendNodes(nodes, parentEl) {
|
|
||||||
let count = 0;
|
|
||||||
this.loopShallowEls(nodes, el => {
|
|
||||||
parentEl.appendChild(el);
|
|
||||||
count++;
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Lifecycle ----
|
|
||||||
/**
|
|
||||||
* @brief Add willUnmount function to node
|
|
||||||
* @param node
|
|
||||||
* @param func
|
|
||||||
*/
|
|
||||||
static addWillUnmount(node, func) {
|
|
||||||
const willUnmountStore = DLStore.global.WillUnmountStore;
|
|
||||||
const currentStore = willUnmountStore[willUnmountStore.length - 1];
|
|
||||||
// ---- If the current store is empty, it means this node is not mutable
|
|
||||||
if (!currentStore) return;
|
|
||||||
currentStore.push(func.bind(null, node));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add didUnmount function to node
|
|
||||||
* @param node
|
|
||||||
* @param func
|
|
||||||
*/
|
|
||||||
static addDidUnmount(node, func) {
|
|
||||||
const didUnmountStore = DLStore.global.DidUnmountStore;
|
|
||||||
const currentStore = didUnmountStore[didUnmountStore.length - 1];
|
|
||||||
// ---- If the current store is empty, it means this node is not mutable
|
|
||||||
if (!currentStore) return;
|
|
||||||
currentStore.push(func.bind(null, node));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add didUnmount function to global store
|
|
||||||
* @param func
|
|
||||||
*/
|
|
||||||
static addDidMount(node, func) {
|
|
||||||
if (!DLStore.global.DidMountStore) DLStore.global.DidMountStore = [];
|
|
||||||
DLStore.global.DidMountStore.push(func.bind(null, node));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Run all didMount functions and reset the global store
|
|
||||||
*/
|
|
||||||
static runDidMount() {
|
|
||||||
const didMountStore = DLStore.global.DidMountStore;
|
|
||||||
if (!didMountStore || didMountStore.length === 0) return;
|
|
||||||
for (let i = didMountStore.length - 1; i >= 0; i--) {
|
|
||||||
didMountStore[i]();
|
|
||||||
}
|
|
||||||
DLStore.global.DidMountStore = [];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
import { DLNode, DLNodeType } from './DLNode';
|
|
||||||
import { DLStore, cached } from './store';
|
|
||||||
|
|
||||||
export class EnvStoreClass {
|
|
||||||
constructor() {
|
|
||||||
this.envs = {};
|
|
||||||
this.currentEnvNodes = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add a node to the current env and merge envs
|
|
||||||
* @param node - The node to add
|
|
||||||
*/
|
|
||||||
addEnvNode(node) {
|
|
||||||
this.currentEnvNodes.push(node);
|
|
||||||
this.mergeEnvs();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Replace the current env with the given nodes and merge envs
|
|
||||||
* @param nodes - The nodes to replace the current environment with
|
|
||||||
*/
|
|
||||||
replaceEnvNodes(nodes) {
|
|
||||||
this.currentEnvNodes = nodes;
|
|
||||||
this.mergeEnvs();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Remove the last node from the current env and merge envs
|
|
||||||
*/
|
|
||||||
removeEnvNode() {
|
|
||||||
this.currentEnvNodes.pop();
|
|
||||||
this.mergeEnvs();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Merge all the envs in currentEnvNodes, inner envs override outer envs
|
|
||||||
*/
|
|
||||||
mergeEnvs() {
|
|
||||||
this.envs = {};
|
|
||||||
this.currentEnvNodes.forEach(envNode => {
|
|
||||||
Object.entries(envNode.envs).forEach(([key, value]) => {
|
|
||||||
this.envs[key] = [value, envNode];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EnvNode extends DLNode {
|
|
||||||
constructor(envs, depsArr) {
|
|
||||||
super(DLNodeType.Env);
|
|
||||||
// Declare a global variable to store the environment variables
|
|
||||||
if (!('DLEnvStore' in DLStore.global)) DLStore.global.DLEnvStore = new EnvStoreClass();
|
|
||||||
|
|
||||||
this.envs = envs;
|
|
||||||
this.depsArr = depsArr;
|
|
||||||
this.updateNodes = new Set();
|
|
||||||
|
|
||||||
DLStore.global.DLEnvStore.addEnvNode(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
cached(deps, name) {
|
|
||||||
if (!deps || !deps.length) return false;
|
|
||||||
if (cached(deps, this.depsArr[name])) return true;
|
|
||||||
this.depsArr[name] = deps;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update a specific env, and update all the comp nodes that depend on this env
|
|
||||||
* @param name - The name of the environment variable to update
|
|
||||||
* @param value - The new value of the environment variable
|
|
||||||
*/
|
|
||||||
updateEnv(name, valueFunc, deps) {
|
|
||||||
if (this.cached(deps, name)) return;
|
|
||||||
const value = valueFunc();
|
|
||||||
this.envs[name] = value;
|
|
||||||
if (DLStore.global.DLEnvStore.currentEnvNodes.includes(this)) {
|
|
||||||
DLStore.global.DLEnvStore.mergeEnvs();
|
|
||||||
}
|
|
||||||
this.updateNodes.forEach(node => {
|
|
||||||
node._$updateEnv(name, value, this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add a node to this.updateNodes, delete the node from this.updateNodes when it unmounts
|
|
||||||
* @param node - The node to add
|
|
||||||
*/
|
|
||||||
addNode(node) {
|
|
||||||
this.updateNodes.add(node);
|
|
||||||
DLNode.addWillUnmount(node, this.updateNodes.delete.bind(this.updateNodes, node));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set this._$nodes, and exit the current env
|
|
||||||
* @param nodes - The nodes to set
|
|
||||||
*/
|
|
||||||
initNodes(nodes) {
|
|
||||||
this._$nodes = nodes;
|
|
||||||
DLStore.global.DLEnvStore.removeEnvNode();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,163 +0,0 @@
|
||||||
import { DLNode } from './DLNode';
|
|
||||||
import { DLStore, cached } from './store';
|
|
||||||
|
|
||||||
function cache(el, key, deps) {
|
|
||||||
if (deps.length === 0) return false;
|
|
||||||
const cacheKey = `$${key}`;
|
|
||||||
if (cached(deps, el[cacheKey])) return true;
|
|
||||||
el[cacheKey] = deps;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Plainly set style
|
|
||||||
* @param el
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setStyle(el, value) {
|
|
||||||
Object.entries(value).forEach(([key, value]) => {
|
|
||||||
if (key.startsWith('--')) {
|
|
||||||
el.style.setProperty(key, value);
|
|
||||||
} else {
|
|
||||||
el.style[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Plainly set dataset
|
|
||||||
* @param el
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setDataset(el, value) {
|
|
||||||
Object.assign(el.dataset, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set HTML property with checking value equality first
|
|
||||||
* @param el
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setHTMLProp(el, key, valueFunc, deps) {
|
|
||||||
// ---- Comparing deps, same value won't trigger
|
|
||||||
// will lead to a bug if the value is set outside of the DLNode
|
|
||||||
// e.g. setHTMLProp(el, "textContent", "value", [])
|
|
||||||
// => el.textContent = "other"
|
|
||||||
// => setHTMLProp(el, "textContent", "value", [])
|
|
||||||
// The value will be set to "other" instead of "value"
|
|
||||||
if (cache(el, key, deps)) return;
|
|
||||||
el[key] = valueFunc();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Plainly set HTML properties
|
|
||||||
* @param el
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setHTMLProps(el, value) {
|
|
||||||
Object.entries(value).forEach(([key, v]) => {
|
|
||||||
if (key === 'style') return setStyle(el, v);
|
|
||||||
if (key === 'dataset') return setDataset(el, v);
|
|
||||||
setHTMLProp(el, key, () => v, []);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set HTML attribute with checking value equality first
|
|
||||||
* @param el
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setHTMLAttr(el, key, valueFunc, deps) {
|
|
||||||
if (cache(el, key, deps)) return;
|
|
||||||
el.setAttribute(key, valueFunc());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Plainly set HTML attributes
|
|
||||||
* @param el
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setHTMLAttrs(el, value) {
|
|
||||||
Object.entries(value).forEach(([key, v]) => {
|
|
||||||
setHTMLAttr(el, key, () => v, []);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set memorized event, store the previous event in el[`$on${key}`], if it exists, remove it first
|
|
||||||
* @param el
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setEvent(el, key, value) {
|
|
||||||
const prevEvent = el[`$on${key}`];
|
|
||||||
if (prevEvent) el.removeEventListener(key, prevEvent);
|
|
||||||
el.addEventListener(key, value);
|
|
||||||
el[`$on${key}`] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function eventHandler(e) {
|
|
||||||
const key = `$$${e.type}`;
|
|
||||||
for (const node of e.composedPath()) {
|
|
||||||
if (node[key]) node[key](e);
|
|
||||||
if (e.cancelBubble) return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function delegateEvent(el, key, value) {
|
|
||||||
if (el[`$$${key}`] === value) return;
|
|
||||||
el[`$$${key}`] = value;
|
|
||||||
if (!DLStore.delegatedEvents.has(key)) {
|
|
||||||
DLStore.delegatedEvents.add(key);
|
|
||||||
DLStore.document.addEventListener(key, eventHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @brief Shortcut for document.createElement
|
|
||||||
* @param tag
|
|
||||||
* @returns HTMLElement
|
|
||||||
*/
|
|
||||||
export function createElement(tag) {
|
|
||||||
return DLStore.document.createElement(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Insert any DLNode into an element, set the _$nodes and append the element to the element's children
|
|
||||||
* @param el
|
|
||||||
* @param node
|
|
||||||
* @param position
|
|
||||||
*/
|
|
||||||
export function insertNode(el, node, position) {
|
|
||||||
// ---- Set _$nodes
|
|
||||||
if (!el._$nodes) el._$nodes = Array.from(el.childNodes);
|
|
||||||
el._$nodes.splice(position, 0, node);
|
|
||||||
|
|
||||||
// ---- Insert nodes' elements
|
|
||||||
const flowIdx = DLNode.getFlowIndexFromNodes(el._$nodes, node);
|
|
||||||
DLNode.appendNodesWithIndex([node], el, flowIdx);
|
|
||||||
// ---- Set parentEl
|
|
||||||
DLNode.addParentEl([node], el);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief An inclusive assign prop function that accepts any type of prop
|
|
||||||
* @param el
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function forwardHTMLProp(el, key, valueFunc, deps) {
|
|
||||||
if (key === 'style') return setStyle(el, valueFunc());
|
|
||||||
if (key === 'dataset') return setDataset(el, valueFunc());
|
|
||||||
if (key === 'element') return;
|
|
||||||
if (key === 'prop') return setHTMLProps(el, valueFunc());
|
|
||||||
if (key === 'attr') return setHTMLAttrs(el, valueFunc());
|
|
||||||
if (key === 'innerHTML') return setHTMLProp(el, 'innerHTML', valueFunc, deps);
|
|
||||||
if (key === 'textContent') return setHTMLProp(el, 'textContent', valueFunc, deps);
|
|
||||||
if (key === 'forwardProp') return;
|
|
||||||
if (key.startsWith('on')) {
|
|
||||||
return setEvent(el, key.slice(2).toLowerCase(), valueFunc());
|
|
||||||
}
|
|
||||||
setHTMLAttr(el, key, valueFunc, deps);
|
|
||||||
}
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { builtinUpdateFunc, inMount, updateCompNode } from './index.js';
|
||||||
|
import { CompNode, HookNode } from './types';
|
||||||
|
import { InulaNodeType } from '@openinula/next-shared';
|
||||||
|
|
||||||
|
export function createHookNode(parent: HookNode | CompNode, bitmap: number): HookNode {
|
||||||
|
return {
|
||||||
|
updateProp: builtinUpdateFunc,
|
||||||
|
updateState: builtinUpdateFunc,
|
||||||
|
__type: InulaNodeType.Hook,
|
||||||
|
props: {},
|
||||||
|
_$nodes: [],
|
||||||
|
bitmap,
|
||||||
|
parent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitUpdate(node: HookNode) {
|
||||||
|
// the new value is not used in the `updateCompNode`, just pass a null
|
||||||
|
updateCompNode(node.parent, null, node.bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function constructHook(
|
||||||
|
node: HookNode,
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
updateState,
|
||||||
|
updateProp,
|
||||||
|
updateContext,
|
||||||
|
getUpdateViews,
|
||||||
|
didUnmount,
|
||||||
|
willUnmount,
|
||||||
|
didMount,
|
||||||
|
}: Pick<
|
||||||
|
HookNode,
|
||||||
|
| 'value'
|
||||||
|
| 'updateState'
|
||||||
|
| 'updateProp'
|
||||||
|
| 'updateContext'
|
||||||
|
| 'getUpdateViews'
|
||||||
|
| 'didUnmount'
|
||||||
|
| 'willUnmount'
|
||||||
|
| 'didMount'
|
||||||
|
>
|
||||||
|
): HookNode {
|
||||||
|
node.value = value;
|
||||||
|
node.updateState = updateState;
|
||||||
|
node.updateProp = updateProp;
|
||||||
|
node.updateContext = updateContext;
|
||||||
|
node.getUpdateViews = getUpdateViews;
|
||||||
|
node.didUnmount = didUnmount;
|
||||||
|
node.willUnmount = willUnmount;
|
||||||
|
node.didMount = didMount;
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { InulaHTMLNode, VNode, TextNode, InulaNode } from './types';
|
||||||
|
|
||||||
|
export const getEl = (node: VNode): Array<InulaHTMLNode | TextNode> => {
|
||||||
|
return toEls(node._$nodes || []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toEls = (nodes: InulaNode[]): Array<InulaHTMLNode | TextNode> => {
|
||||||
|
const els: Array<InulaHTMLNode | TextNode> = [];
|
||||||
|
loopShallowEls(nodes, el => {
|
||||||
|
els.push(el);
|
||||||
|
});
|
||||||
|
return els;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loopShallowEls = (nodes: InulaNode[], runFunc: (el: InulaHTMLNode | TextNode) => void): void => {
|
||||||
|
const stack: Array<InulaNode> = [...nodes].reverse();
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const node = stack.pop()!;
|
||||||
|
if (node instanceof HTMLElement || node instanceof Text) {
|
||||||
|
runFunc(node);
|
||||||
|
} else if (node._$nodes) {
|
||||||
|
stack.push(...[...node._$nodes].reverse());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addParentEl = (nodes: Array<InulaNode>, parentEl: HTMLElement): void => {
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if ('__type' in node) {
|
||||||
|
node._$parentEl = parentEl as InulaHTMLNode;
|
||||||
|
node._$nodes && addParentEl(node._$nodes, parentEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFlowIndexFromNodes = (nodes: InulaNode[], stopNode?: InulaNode): number => {
|
||||||
|
let index = 0;
|
||||||
|
const stack: InulaNode[] = [...nodes].reverse();
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const node = stack.pop()!;
|
||||||
|
if (node === stopNode) break;
|
||||||
|
if ('__type' in node) {
|
||||||
|
node._$nodes && stack.push(...[...node._$nodes].reverse());
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appendNodesWithSibling = (nodes: Array<InulaNode>, parentEl: HTMLElement, nextSibling?: Node): number => {
|
||||||
|
if (nextSibling) return insertNodesBefore(nodes, parentEl, nextSibling);
|
||||||
|
return appendNodes(nodes, parentEl);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appendNodesWithIndex = (
|
||||||
|
nodes: InulaNode[],
|
||||||
|
parentEl: HTMLElement,
|
||||||
|
index: number,
|
||||||
|
length?: number
|
||||||
|
): number => {
|
||||||
|
length = length ?? parentEl.childNodes.length;
|
||||||
|
if (length !== index) return insertNodesBefore(nodes, parentEl, parentEl.childNodes[index]);
|
||||||
|
return appendNodes(nodes, parentEl);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertNodesBefore = (nodes: InulaNode[], parentEl: HTMLElement, nextSibling: Node): number => {
|
||||||
|
let count = 0;
|
||||||
|
loopShallowEls(nodes, el => {
|
||||||
|
parentEl.insertBefore(el, nextSibling);
|
||||||
|
count++;
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendNodes = (nodes: InulaNode[], parentEl: HTMLElement): number => {
|
||||||
|
let count = 0;
|
||||||
|
loopShallowEls(nodes, el => {
|
||||||
|
parentEl.appendChild(el);
|
||||||
|
count++;
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
};
|
|
@ -1,69 +0,0 @@
|
||||||
import { DLNodeType } from '../DLNode';
|
|
||||||
import { FlatNode } from './FlatNode';
|
|
||||||
|
|
||||||
export class CondNode extends FlatNode {
|
|
||||||
/**
|
|
||||||
* @brief Constructor, If type, accept a function that returns a list of nodes
|
|
||||||
* @param caseFunc
|
|
||||||
*/
|
|
||||||
constructor(depNum, condFunc) {
|
|
||||||
super(DLNodeType.Cond);
|
|
||||||
this.depNum = depNum;
|
|
||||||
this.cond = -1;
|
|
||||||
this.condFunc = condFunc;
|
|
||||||
this.initUnmountStore();
|
|
||||||
this._$nodes = this.condFunc(this);
|
|
||||||
this.setUnmountFuncs();
|
|
||||||
|
|
||||||
// ---- Add to the global UnmountStore
|
|
||||||
CondNode.addWillUnmount(this, this.runWillUnmount.bind(this));
|
|
||||||
CondNode.addDidUnmount(this, this.runDidUnmount.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update the nodes in the environment
|
|
||||||
*/
|
|
||||||
updateCond(key) {
|
|
||||||
// ---- Need to save prev unmount funcs because we can't put removeNodes before geneNewNodesInEnv
|
|
||||||
// The reason is that if it didn't change, we don't need to unmount or remove the nodes
|
|
||||||
const prevFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
|
|
||||||
const newNodes = this.geneNewNodesInEnv(() => this.condFunc(this));
|
|
||||||
|
|
||||||
// ---- If the new nodes are the same as the old nodes, we only need to update children
|
|
||||||
if (this.didntChange) {
|
|
||||||
[this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
|
|
||||||
this.didntChange = false;
|
|
||||||
this.updateFunc?.(this.depNum, key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ---- Remove old nodes
|
|
||||||
const newFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
|
|
||||||
[this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
|
|
||||||
this._$nodes && this._$nodes.length > 0 && this.removeNodes(this._$nodes);
|
|
||||||
[this.willUnmountFuncs, this.didUnmountFuncs] = newFuncs;
|
|
||||||
|
|
||||||
if (newNodes.length === 0) {
|
|
||||||
// ---- No branch has been taken
|
|
||||||
this._$nodes = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ---- Add new nodes
|
|
||||||
const parentEl = this._$parentEl;
|
|
||||||
// ---- Faster append with nextSibling rather than flowIndex
|
|
||||||
const flowIndex = CondNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
|
||||||
|
|
||||||
const nextSibling = parentEl.childNodes[flowIndex];
|
|
||||||
CondNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
|
||||||
CondNode.runDidMount();
|
|
||||||
this._$nodes = newNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The update function of IfNode's childNodes is stored in the first child node
|
|
||||||
* @param changed
|
|
||||||
*/
|
|
||||||
update(changed) {
|
|
||||||
if (!(~this.depNum & changed)) return;
|
|
||||||
this.updateFunc?.(changed);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { appendNodesWithSibling, getFlowIndexFromNodes } from '../InulaNode.js';
|
||||||
|
import { addDidUnmount, addWillUnmount, runDidMount } from '../lifecycle.js';
|
||||||
|
import {
|
||||||
|
geneNewNodesInEnvWithUnmount,
|
||||||
|
removeNodesWithUnmount,
|
||||||
|
runLifeCycle,
|
||||||
|
setUnmountFuncs,
|
||||||
|
} from './mutableHandler.js';
|
||||||
|
import { CondNode, VNode } from '../types';
|
||||||
|
import { geneNewNodesInCtx, getSavedCtxNodes, removeNodes } from './mutableHandler.js';
|
||||||
|
import { startUnmountScope } from '../lifecycle.js';
|
||||||
|
import { InulaNodeType } from '@openinula/next-shared';
|
||||||
|
|
||||||
|
export function createCondNode(depNum: number, condFunc: (condNode: CondNode) => VNode[]) {
|
||||||
|
startUnmountScope();
|
||||||
|
|
||||||
|
const condNode: CondNode = {
|
||||||
|
__type: InulaNodeType.Cond,
|
||||||
|
cond: -1,
|
||||||
|
didntChange: false,
|
||||||
|
depNum,
|
||||||
|
condFunc,
|
||||||
|
savedContextNodes: getSavedCtxNodes(),
|
||||||
|
_$nodes: [],
|
||||||
|
willUnmountFuncs: [],
|
||||||
|
didUnmountFuncs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
condNode._$nodes = condFunc(condNode);
|
||||||
|
setUnmountFuncs(condNode);
|
||||||
|
|
||||||
|
if (condNode.willUnmountFuncs) {
|
||||||
|
// ---- Add condNode willUnmount func to the global UnmountStore
|
||||||
|
addWillUnmount(condNode, runLifeCycle.bind(condNode, condNode.willUnmountFuncs));
|
||||||
|
}
|
||||||
|
if (condNode.didUnmountFuncs) {
|
||||||
|
// ---- Add condNode didUnmount func to the global UnmountStore
|
||||||
|
addDidUnmount(condNode, runLifeCycle.bind(condNode, condNode.didUnmountFuncs));
|
||||||
|
}
|
||||||
|
|
||||||
|
return condNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief the condition changed, update children of current branch
|
||||||
|
*/
|
||||||
|
export function updateCondChildren(condNode: CondNode, changed: number) {
|
||||||
|
if (condNode.depNum & changed) {
|
||||||
|
// If the depNum of the condition has changed, directly return because node already updated in the `updateBranch`
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
condNode.updateFunc?.(changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The update function of CondNode's childNodes when the condition changed
|
||||||
|
*/
|
||||||
|
export function updateCondNode(condNode: CondNode) {
|
||||||
|
// ---- Need to save prev unmount funcs because we can't put removeNodes before geneNewNodesInEnv
|
||||||
|
// The reason is that if it didn't change, we don't need to unmount or remove the nodes
|
||||||
|
const prevFuncs = [condNode.willUnmountFuncs, condNode.didUnmountFuncs];
|
||||||
|
const newNodes = geneNewNodesInEnvWithUnmount(condNode, () => condNode.condFunc(condNode));
|
||||||
|
|
||||||
|
// ---- If the new nodes are the same as the old nodes, we only need to update children
|
||||||
|
if (condNode.didntChange) {
|
||||||
|
[condNode.willUnmountFuncs, condNode.didUnmountFuncs] = prevFuncs;
|
||||||
|
condNode.didntChange = false;
|
||||||
|
condNode.updateFunc?.(condNode.depNum);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ---- Remove old nodes
|
||||||
|
const newFuncs = [condNode.willUnmountFuncs, condNode.didUnmountFuncs];
|
||||||
|
[condNode.willUnmountFuncs, condNode.didUnmountFuncs] = prevFuncs;
|
||||||
|
condNode._$nodes && condNode._$nodes.length > 0 && removeNodesWithUnmount(condNode, condNode._$nodes);
|
||||||
|
[condNode.willUnmountFuncs, condNode.didUnmountFuncs] = newFuncs;
|
||||||
|
|
||||||
|
if (newNodes.length === 0) {
|
||||||
|
// ---- No branch has been taken
|
||||||
|
condNode._$nodes = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ---- Add new nodes
|
||||||
|
const parentEl = condNode._$parentEl!;
|
||||||
|
// ---- Faster append with nextSibling rather than flowIndex
|
||||||
|
const flowIndex = getFlowIndexFromNodes(parentEl._$nodes, condNode);
|
||||||
|
|
||||||
|
const nextSibling = parentEl.childNodes[flowIndex];
|
||||||
|
appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
||||||
|
runDidMount();
|
||||||
|
condNode._$nodes = newNodes;
|
||||||
|
}
|
|
@ -1,86 +0,0 @@
|
||||||
import { DLNodeType } from '../DLNode';
|
|
||||||
import { FlatNode } from './FlatNode';
|
|
||||||
import { DLStore, cached } from '../store';
|
|
||||||
|
|
||||||
export class ExpNode extends FlatNode {
|
|
||||||
/**
|
|
||||||
* @brief Constructor, Exp type, accept a function that returns a list of nodes
|
|
||||||
* @param nodesFunc
|
|
||||||
*/
|
|
||||||
constructor(value, deps) {
|
|
||||||
super(DLNodeType.Exp);
|
|
||||||
this.initUnmountStore();
|
|
||||||
this._$nodes = ExpNode.formatNodes(value);
|
|
||||||
this.setUnmountFuncs();
|
|
||||||
this.deps = this.parseDeps(deps);
|
|
||||||
// ---- Add to the global UnmountStore
|
|
||||||
ExpNode.addWillUnmount(this, this.runWillUnmount.bind(this));
|
|
||||||
ExpNode.addDidUnmount(this, this.runDidUnmount.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
parseDeps(deps) {
|
|
||||||
return deps.map(dep => {
|
|
||||||
// ---- CompNode
|
|
||||||
if (dep?.prototype?._$init) return dep.toString();
|
|
||||||
// ---- SnippetNode
|
|
||||||
if (dep?.propViewFunc) return dep.propViewFunc.toString();
|
|
||||||
return dep;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cache(deps) {
|
|
||||||
if (!deps || !deps.length) return false;
|
|
||||||
deps = this.parseDeps(deps);
|
|
||||||
if (cached(deps, this.deps)) return true;
|
|
||||||
this.deps = deps;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @brief Generate new nodes and replace the old nodes
|
|
||||||
*/
|
|
||||||
update(valueFunc, deps) {
|
|
||||||
if (this.cache(deps)) return;
|
|
||||||
this.removeNodes(this._$nodes);
|
|
||||||
const newNodes = this.geneNewNodesInEnv(() => ExpNode.formatNodes(valueFunc()));
|
|
||||||
if (newNodes.length === 0) {
|
|
||||||
this._$nodes = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Add new nodes
|
|
||||||
const parentEl = this._$parentEl;
|
|
||||||
const flowIndex = ExpNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
|
||||||
const nextSibling = parentEl.childNodes[flowIndex];
|
|
||||||
ExpNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
|
||||||
ExpNode.runDidMount();
|
|
||||||
|
|
||||||
this._$nodes = newNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Format the nodes
|
|
||||||
* @param nodes
|
|
||||||
* @returns New nodes
|
|
||||||
*/
|
|
||||||
static formatNodes(nodes) {
|
|
||||||
if (!Array.isArray(nodes)) nodes = [nodes];
|
|
||||||
return (
|
|
||||||
nodes
|
|
||||||
// ---- Flatten the nodes
|
|
||||||
.flat(1)
|
|
||||||
// ---- Filter out empty nodes
|
|
||||||
.filter(node => node !== undefined && node !== null && typeof node !== 'boolean')
|
|
||||||
.map(node => {
|
|
||||||
// ---- If the node is a string, number or bigint, convert it to a text node
|
|
||||||
if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
|
|
||||||
return DLStore.document.createTextNode(`${node}`);
|
|
||||||
}
|
|
||||||
// ---- If the node has PropView, call it to get the view
|
|
||||||
if ('propViewFunc' in node) return node.build();
|
|
||||||
return node;
|
|
||||||
})
|
|
||||||
// ---- Flatten the nodes again
|
|
||||||
.flat(1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { InulaNodeType } from '@openinula/next-shared';
|
||||||
|
import { appendNodesWithSibling, getFlowIndexFromNodes } from '../InulaNode';
|
||||||
|
import { addDidUnmount, addWillUnmount, runDidMount } from '../lifecycle';
|
||||||
|
import { equal } from '../equal';
|
||||||
|
import { ChildrenNode, ExpNode, VNode, TextNode, InulaNode } from '../types';
|
||||||
|
import { removeNodesWithUnmount, runLifeCycle, setUnmountFuncs } from './mutableHandler';
|
||||||
|
import { geneNewNodesInCtx, getSavedCtxNodes, removeNodes } from './mutableHandler';
|
||||||
|
import { startUnmountScope } from '../lifecycle';
|
||||||
|
import { buildChildren } from '../ChildrenNode';
|
||||||
|
import { createTextNode } from '../renderer/dom';
|
||||||
|
|
||||||
|
function isChildrenNode(node: any): node is ChildrenNode {
|
||||||
|
return node.__type === InulaNodeType.Children;
|
||||||
|
}
|
||||||
|
function getExpressionResult(fn: () => Array<InulaNode>) {
|
||||||
|
let nodes = fn();
|
||||||
|
if (!Array.isArray(nodes)) nodes = [nodes];
|
||||||
|
return (
|
||||||
|
nodes
|
||||||
|
// ---- Flatten the nodes
|
||||||
|
.flat(1)
|
||||||
|
// ---- Filter out empty nodes
|
||||||
|
.filter(node => node !== undefined && node !== null && typeof node !== 'boolean')
|
||||||
|
.map(node => {
|
||||||
|
// ---- If the node is a string, number or bigint, convert it to a text node
|
||||||
|
if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
|
||||||
|
return createTextNode(`${node}`);
|
||||||
|
}
|
||||||
|
// TODO ---- If the node has PropView, call it to get the view,
|
||||||
|
if (isChildrenNode(node)) return buildChildren(node);
|
||||||
|
return node;
|
||||||
|
})
|
||||||
|
// ---- Flatten the nodes again
|
||||||
|
.flat(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function createExpNode(value: () => VNode[], deps: unknown[]) {
|
||||||
|
startUnmountScope();
|
||||||
|
|
||||||
|
const expNode: ExpNode = {
|
||||||
|
__type: InulaNodeType.Exp,
|
||||||
|
_$nodes: getExpressionResult(value),
|
||||||
|
deps,
|
||||||
|
savedContextNodes: getSavedCtxNodes(),
|
||||||
|
willUnmountFuncs: [],
|
||||||
|
didUnmountFuncs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
setUnmountFuncs(expNode);
|
||||||
|
|
||||||
|
if (expNode.willUnmountFuncs) {
|
||||||
|
// ---- Add expNode willUnmount func to the global UnmountStore
|
||||||
|
addWillUnmount(expNode, runLifeCycle.bind(expNode, expNode.willUnmountFuncs));
|
||||||
|
}
|
||||||
|
if (expNode.didUnmountFuncs) {
|
||||||
|
// ---- Add expNode didUnmount func to the global UnmountStore
|
||||||
|
addDidUnmount(expNode, runLifeCycle.bind(expNode, expNode.didUnmountFuncs));
|
||||||
|
}
|
||||||
|
return expNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateExpNode(expNode: ExpNode, valueFunc: () => VNode[], deps: unknown[]) {
|
||||||
|
if (cache(expNode, deps)) return;
|
||||||
|
removeNodesWithUnmount(expNode, expNode._$nodes);
|
||||||
|
const newNodes = geneNewNodesInCtx(expNode, () => getExpressionResult(valueFunc));
|
||||||
|
if (newNodes.length === 0) {
|
||||||
|
expNode._$nodes = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parentEl = expNode._$parentEl!;
|
||||||
|
const flowIndex = getFlowIndexFromNodes(parentEl._$nodes, expNode);
|
||||||
|
const nextSibling = parentEl.childNodes[flowIndex];
|
||||||
|
appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
||||||
|
runDidMount();
|
||||||
|
|
||||||
|
expNode._$nodes = newNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cache(expNode: ExpNode, deps: unknown[]) {
|
||||||
|
if (!deps || !deps.length) return false;
|
||||||
|
if (equal(deps, expNode.deps)) return true;
|
||||||
|
expNode.deps = deps;
|
||||||
|
return false;
|
||||||
|
}
|
|
@ -1,33 +0,0 @@
|
||||||
import { DLStore } from '../store';
|
|
||||||
import { MutableNode } from './MutableNode';
|
|
||||||
|
|
||||||
export class FlatNode extends MutableNode {
|
|
||||||
willUnmountFuncs = [];
|
|
||||||
didUnmountFuncs = [];
|
|
||||||
|
|
||||||
setUnmountFuncs() {
|
|
||||||
this.willUnmountFuncs = DLStore.global.WillUnmountStore.pop();
|
|
||||||
this.didUnmountFuncs = DLStore.global.DidUnmountStore.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
runWillUnmount() {
|
|
||||||
for (let i = 0; i < this.willUnmountFuncs.length; i++) this.willUnmountFuncs[i]();
|
|
||||||
}
|
|
||||||
|
|
||||||
runDidUnmount() {
|
|
||||||
for (let i = this.didUnmountFuncs.length - 1; i >= 0; i--) this.didUnmountFuncs[i]();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeNodes(nodes) {
|
|
||||||
this.runWillUnmount();
|
|
||||||
super.removeNodes(nodes);
|
|
||||||
this.runDidUnmount();
|
|
||||||
}
|
|
||||||
|
|
||||||
geneNewNodesInEnv(newNodesFunc) {
|
|
||||||
this.initUnmountStore();
|
|
||||||
const nodes = super.geneNewNodesInEnv(newNodesFunc);
|
|
||||||
this.setUnmountFuncs();
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,406 +0,0 @@
|
||||||
import { DLNodeType } from '../DLNode';
|
|
||||||
import { DLStore } from '../store';
|
|
||||||
import { MutableNode } from './MutableNode';
|
|
||||||
|
|
||||||
export class ForNode extends MutableNode {
|
|
||||||
array;
|
|
||||||
nodeFunc;
|
|
||||||
depNum;
|
|
||||||
|
|
||||||
nodesMap = new Map();
|
|
||||||
updateArr = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Getter for nodes
|
|
||||||
*/
|
|
||||||
get _$nodes() {
|
|
||||||
const nodes = [];
|
|
||||||
for (let idx = 0; idx < this.array.length; idx++) {
|
|
||||||
nodes.push(...this.nodesMap.get(this.keys?.[idx] ?? idx));
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Constructor, For type
|
|
||||||
* @param array
|
|
||||||
* @param nodeFunc
|
|
||||||
* @param keys
|
|
||||||
*/
|
|
||||||
constructor(array, depNum, keys, nodeFunc) {
|
|
||||||
super(DLNodeType.For);
|
|
||||||
this.array = [...array];
|
|
||||||
this.keys = keys;
|
|
||||||
this.depNum = depNum;
|
|
||||||
this.addNodeFunc(nodeFunc);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief To be called immediately after the constructor
|
|
||||||
* @param nodeFunc
|
|
||||||
*/
|
|
||||||
addNodeFunc(nodeFunc) {
|
|
||||||
this.nodeFunc = nodeFunc;
|
|
||||||
this.array.forEach((item, idx) => {
|
|
||||||
this.initUnmountStore();
|
|
||||||
const key = this.keys?.[idx] ?? idx;
|
|
||||||
const nodes = nodeFunc(item, this.updateArr, idx);
|
|
||||||
this.nodesMap.set(key, nodes);
|
|
||||||
this.setUnmountMap(key);
|
|
||||||
});
|
|
||||||
// ---- For nested ForNode, the whole strategy is just like EnvStore
|
|
||||||
// we use array of function array to create "environment", popping and pushing
|
|
||||||
ForNode.addWillUnmount(this, this.runAllWillUnmount.bind(this));
|
|
||||||
ForNode.addDidUnmount(this, this.runAllDidUnmount.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update the view related to one item in the array
|
|
||||||
* @param nodes
|
|
||||||
* @param item
|
|
||||||
*/
|
|
||||||
updateItem(idx, array, changed) {
|
|
||||||
// ---- The update function of ForNode's childNodes is stored in the first child node
|
|
||||||
this.updateArr[idx]?.(changed ?? this.depNum, array[idx]);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateItems(changed) {
|
|
||||||
for (let idx = 0; idx < this.array.length; idx++) {
|
|
||||||
this.updateItem(idx, this.array, changed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Non-array update function
|
|
||||||
* @param changed
|
|
||||||
*/
|
|
||||||
update(changed) {
|
|
||||||
// ---- e.g. this.depNum -> 1110 changed-> 1010
|
|
||||||
// ~this.depNum & changed -> ~1110 & 1010 -> 0000
|
|
||||||
// no update because depNum contains all the changed
|
|
||||||
// ---- e.g. this.depNum -> 1110 changed-> 1101
|
|
||||||
// ~this.depNum & changed -> ~1110 & 1101 -> 0001
|
|
||||||
// update because depNum doesn't contain all the changed
|
|
||||||
if (!(~this.depNum & changed)) return;
|
|
||||||
this.updateItems(changed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Array-related update function
|
|
||||||
* @param newArray
|
|
||||||
* @param newKeys
|
|
||||||
*/
|
|
||||||
updateArray(newArray, newKeys) {
|
|
||||||
if (newKeys) {
|
|
||||||
this.updateWithKey(newArray, newKeys);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.updateWithOutKey(newArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Shortcut to generate new nodes with idx and key
|
|
||||||
*/
|
|
||||||
getNewNodes(idx, key, array, updateArr) {
|
|
||||||
this.initUnmountStore();
|
|
||||||
const nodes = this.geneNewNodesInEnv(() => this.nodeFunc(array[idx], updateArr ?? this.updateArr, idx));
|
|
||||||
this.setUnmountMap(key);
|
|
||||||
this.nodesMap.set(key, nodes);
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set the unmount map by getting the last unmount map from the global store
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
setUnmountMap(key) {
|
|
||||||
const willUnmountMap = DLStore.global.WillUnmountStore.pop();
|
|
||||||
if (willUnmountMap && willUnmountMap.length > 0) {
|
|
||||||
if (!this.willUnmountMap) this.willUnmountMap = new Map();
|
|
||||||
this.willUnmountMap.set(key, willUnmountMap);
|
|
||||||
}
|
|
||||||
const didUnmountMap = DLStore.global.DidUnmountStore.pop();
|
|
||||||
if (didUnmountMap && didUnmountMap.length > 0) {
|
|
||||||
if (!this.didUnmountMap) this.didUnmountMap = new Map();
|
|
||||||
this.didUnmountMap.set(key, didUnmountMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Run all the unmount functions and clear the unmount map
|
|
||||||
*/
|
|
||||||
runAllWillUnmount() {
|
|
||||||
if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
|
|
||||||
this.willUnmountMap.forEach(funcs => {
|
|
||||||
for (let i = 0; i < funcs.length; i++) funcs[i]?.();
|
|
||||||
});
|
|
||||||
this.willUnmountMap.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Run all the unmount functions and clear the unmount map
|
|
||||||
*/
|
|
||||||
runAllDidUnmount() {
|
|
||||||
if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
|
|
||||||
this.didUnmountMap.forEach(funcs => {
|
|
||||||
for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
|
|
||||||
});
|
|
||||||
this.didUnmountMap.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Run the unmount functions of the given key
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
runWillUnmount(key) {
|
|
||||||
if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
|
|
||||||
const funcs = this.willUnmountMap.get(key);
|
|
||||||
if (!funcs) return;
|
|
||||||
for (let i = 0; i < funcs.length; i++) funcs[i]?.();
|
|
||||||
this.willUnmountMap.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Run the unmount functions of the given key
|
|
||||||
*/
|
|
||||||
runDidUnmount(key) {
|
|
||||||
if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
|
|
||||||
const funcs = this.didUnmountMap.get(key);
|
|
||||||
if (!funcs) return;
|
|
||||||
for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
|
|
||||||
this.didUnmountMap.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Remove nodes from parentEl and run willUnmount and didUnmount
|
|
||||||
* @param nodes
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
removeNodes(nodes, key) {
|
|
||||||
this.runWillUnmount(key);
|
|
||||||
super.removeNodes(nodes);
|
|
||||||
this.runDidUnmount(key);
|
|
||||||
this.nodesMap.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update the nodes without keys
|
|
||||||
* @param newArray
|
|
||||||
*/
|
|
||||||
updateWithOutKey(newArray) {
|
|
||||||
const preLength = this.array.length;
|
|
||||||
const currLength = newArray.length;
|
|
||||||
|
|
||||||
if (preLength === currLength) {
|
|
||||||
// ---- If the length is the same, we only need to update the nodes
|
|
||||||
for (let idx = 0; idx < this.array.length; idx++) {
|
|
||||||
this.updateItem(idx, newArray);
|
|
||||||
}
|
|
||||||
this.array = [...newArray];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const parentEl = this._$parentEl;
|
|
||||||
// ---- If the new array is longer, add new nodes directly
|
|
||||||
if (preLength < currLength) {
|
|
||||||
let flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
|
||||||
// ---- Calling parentEl.childNodes.length is time-consuming,
|
|
||||||
// so we use a length variable to store the length
|
|
||||||
const length = parentEl.childNodes.length;
|
|
||||||
for (let idx = 0; idx < currLength; idx++) {
|
|
||||||
if (idx < preLength) {
|
|
||||||
flowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(idx));
|
|
||||||
this.updateItem(idx, newArray);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const newNodes = this.getNewNodes(idx, idx, newArray);
|
|
||||||
ForNode.appendNodesWithIndex(newNodes, parentEl, flowIndex, length);
|
|
||||||
}
|
|
||||||
ForNode.runDidMount();
|
|
||||||
this.array = [...newArray];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Update the nodes first
|
|
||||||
for (let idx = 0; idx < currLength; idx++) {
|
|
||||||
this.updateItem(idx, newArray);
|
|
||||||
}
|
|
||||||
// ---- If the new array is shorter, remove the extra nodes
|
|
||||||
for (let idx = currLength; idx < preLength; idx++) {
|
|
||||||
const nodes = this.nodesMap.get(idx);
|
|
||||||
this.removeNodes(nodes, idx);
|
|
||||||
}
|
|
||||||
this.updateArr.splice(currLength, preLength - currLength);
|
|
||||||
this.array = [...newArray];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update the nodes with keys
|
|
||||||
* @param newArray
|
|
||||||
* @param newKeys
|
|
||||||
*/
|
|
||||||
updateWithKey(newArray, newKeys) {
|
|
||||||
if (newKeys.length !== new Set(newKeys).size) {
|
|
||||||
throw new Error('DLight: Duplicate keys in for loop are not allowed');
|
|
||||||
}
|
|
||||||
const prevKeys = this.keys;
|
|
||||||
this.keys = newKeys;
|
|
||||||
|
|
||||||
if (ForNode.arrayEqual(prevKeys, this.keys)) {
|
|
||||||
// ---- If the keys are the same, we only need to update the nodes
|
|
||||||
for (let idx = 0; idx < newArray.length; idx++) {
|
|
||||||
this.updateItem(idx, newArray);
|
|
||||||
}
|
|
||||||
this.array = [...newArray];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentEl = this._$parentEl;
|
|
||||||
|
|
||||||
// ---- No nodes after, delete all nodes
|
|
||||||
if (this.keys.length === 0) {
|
|
||||||
const parentNodes = parentEl._$nodes ?? [];
|
|
||||||
if (parentNodes.length === 1 && parentNodes[0] === this) {
|
|
||||||
// ---- ForNode is the only node in the parent node
|
|
||||||
// Frequently used in real life scenarios because we tend to always wrap for with a div element,
|
|
||||||
// so we optimize it here
|
|
||||||
this.runAllWillUnmount();
|
|
||||||
parentEl.innerHTML = '';
|
|
||||||
this.runAllDidUnmount();
|
|
||||||
} else {
|
|
||||||
for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
|
|
||||||
const prevKey = prevKeys[prevIdx];
|
|
||||||
this.removeNodes(this.nodesMap.get(prevKey), prevKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.nodesMap.clear();
|
|
||||||
this.updateArr = [];
|
|
||||||
this.array = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Record how many nodes are before this ForNode with the same parentNode
|
|
||||||
const flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
|
||||||
|
|
||||||
// ---- No nodes before, append all nodes
|
|
||||||
if (prevKeys.length === 0) {
|
|
||||||
const nextSibling = parentEl.childNodes[flowIndex];
|
|
||||||
for (let idx = 0; idx < this.keys.length; idx++) {
|
|
||||||
const newNodes = this.getNewNodes(idx, this.keys[idx], newArray);
|
|
||||||
ForNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
|
||||||
}
|
|
||||||
ForNode.runDidMount();
|
|
||||||
this.array = [...newArray];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shuffleKeys = [];
|
|
||||||
const newUpdateArr = [];
|
|
||||||
|
|
||||||
// ---- 1. Delete the nodes that are no longer in the array
|
|
||||||
for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
|
|
||||||
const prevKey = prevKeys[prevIdx];
|
|
||||||
if (this.keys.includes(prevKey)) {
|
|
||||||
shuffleKeys.push(prevKey);
|
|
||||||
newUpdateArr.push(this.updateArr[prevIdx]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
this.removeNodes(this.nodesMap.get(prevKey), prevKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 2. Add the nodes that are not in the array but in the new array
|
|
||||||
// ---- Calling parentEl.childNodes.length is time-consuming,
|
|
||||||
// so we use a length variable to store the length
|
|
||||||
let length = parentEl.childNodes.length;
|
|
||||||
let newFlowIndex = flowIndex;
|
|
||||||
for (let idx = 0; idx < this.keys.length; idx++) {
|
|
||||||
const key = this.keys[idx];
|
|
||||||
const prevIdx = shuffleKeys.indexOf(key);
|
|
||||||
if (prevIdx !== -1) {
|
|
||||||
// ---- These nodes are already in the parentEl,
|
|
||||||
// and we need to keep track of their flowIndex
|
|
||||||
newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
|
|
||||||
newUpdateArr[prevIdx]?.(this.depNum, newArray[idx]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// ---- Insert updateArr first because in getNewNode the updateFunc will replace this null
|
|
||||||
newUpdateArr.splice(idx, 0, null);
|
|
||||||
const newNodes = this.getNewNodes(idx, key, newArray, newUpdateArr);
|
|
||||||
// ---- Add the new nodes
|
|
||||||
shuffleKeys.splice(idx, 0, key);
|
|
||||||
|
|
||||||
const count = ForNode.appendNodesWithIndex(newNodes, parentEl, newFlowIndex, length);
|
|
||||||
newFlowIndex += count;
|
|
||||||
length += count;
|
|
||||||
}
|
|
||||||
ForNode.runDidMount();
|
|
||||||
|
|
||||||
// ---- After adding and deleting, the only thing left is to reorder the nodes,
|
|
||||||
// but if the keys are the same, we don't need to reorder
|
|
||||||
if (ForNode.arrayEqual(this.keys, shuffleKeys)) {
|
|
||||||
this.array = [...newArray];
|
|
||||||
this.updateArr = newUpdateArr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
newFlowIndex = flowIndex;
|
|
||||||
const bufferNodes = new Map();
|
|
||||||
// ---- 3. Replace the nodes in the same position using Fisher-Yates shuffle algorithm
|
|
||||||
for (let idx = 0; idx < this.keys.length; idx++) {
|
|
||||||
const key = this.keys[idx];
|
|
||||||
const prevIdx = shuffleKeys.indexOf(key);
|
|
||||||
|
|
||||||
const bufferedNode = bufferNodes.get(key);
|
|
||||||
if (bufferedNode) {
|
|
||||||
// ---- We need to add the flowIndex of the bufferedNode,
|
|
||||||
// because the bufferedNode is in the parentEl and the new position is ahead of the previous position
|
|
||||||
const bufferedFlowIndex = ForNode.getFlowIndexFromNodes(bufferedNode);
|
|
||||||
const lastEl = ForNode.toEls(bufferedNode).pop();
|
|
||||||
const nextSibling = parentEl.childNodes[newFlowIndex + bufferedFlowIndex];
|
|
||||||
if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
|
|
||||||
// ---- If the node is buffered, we need to add it to the parentEl
|
|
||||||
ForNode.insertNodesBefore(bufferedNode, parentEl, nextSibling);
|
|
||||||
}
|
|
||||||
// ---- So the added length is the length of the bufferedNode
|
|
||||||
newFlowIndex += bufferedFlowIndex;
|
|
||||||
delete bufferNodes[idx];
|
|
||||||
} else if (prevIdx === idx) {
|
|
||||||
// ---- If the node is in the same position, we don't need to do anything
|
|
||||||
newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
// ---- If the node is not in the same position, we need to buffer it
|
|
||||||
// We buffer the node of the previous position, and then replace it with the node of the current position
|
|
||||||
const prevKey = shuffleKeys[idx];
|
|
||||||
bufferNodes.set(prevKey, this.nodesMap.get(prevKey));
|
|
||||||
// ---- Length would never change, and the last will always be in the same position,
|
|
||||||
// so it'll always be insertBefore instead of appendChild
|
|
||||||
const childNodes = this.nodesMap.get(key);
|
|
||||||
const lastEl = ForNode.toEls(childNodes).pop();
|
|
||||||
const nextSibling = parentEl.childNodes[newFlowIndex];
|
|
||||||
if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
|
|
||||||
newFlowIndex += ForNode.insertNodesBefore(childNodes, parentEl, nextSibling);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ---- Swap the keys
|
|
||||||
const tempKey = shuffleKeys[idx];
|
|
||||||
shuffleKeys[idx] = shuffleKeys[prevIdx];
|
|
||||||
shuffleKeys[prevIdx] = tempKey;
|
|
||||||
const tempUpdateFunc = newUpdateArr[idx];
|
|
||||||
newUpdateArr[idx] = newUpdateArr[prevIdx];
|
|
||||||
newUpdateArr[prevIdx] = tempUpdateFunc;
|
|
||||||
}
|
|
||||||
this.array = [...newArray];
|
|
||||||
this.updateArr = newUpdateArr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Compare two arrays
|
|
||||||
* @param arr1
|
|
||||||
* @param arr2
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
static arrayEqual(arr1, arr2) {
|
|
||||||
if (arr1.length !== arr2.length) return false;
|
|
||||||
return arr1.every((item, idx) => item === arr2[idx]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,373 @@
|
||||||
|
import { InulaNodeType } from '@openinula/next-shared';
|
||||||
|
import {
|
||||||
|
appendNodesWithIndex,
|
||||||
|
appendNodesWithSibling,
|
||||||
|
getFlowIndexFromNodes,
|
||||||
|
insertNodesBefore,
|
||||||
|
toEls,
|
||||||
|
} from '../InulaNode';
|
||||||
|
import { addDidUnmount, addWillUnmount, endUnmountScope, runDidMount } from '../lifecycle';
|
||||||
|
import { ForNode, InulaNode, VNode } from '../types';
|
||||||
|
import { geneNewNodesInCtx, getSavedCtxNodes } from './mutableHandler';
|
||||||
|
import { startUnmountScope } from '../lifecycle';
|
||||||
|
import { removeNodes as removeMutableNodes } from './mutableHandler';
|
||||||
|
|
||||||
|
export function createForNode<T>(
|
||||||
|
array: T[],
|
||||||
|
depNum: number,
|
||||||
|
keys: number[],
|
||||||
|
nodeFunc: (item: T, idx: number, updateArr: any[]) => VNode[]
|
||||||
|
) {
|
||||||
|
const forNode: ForNode<T> = {
|
||||||
|
__type: InulaNodeType.For,
|
||||||
|
array: [...array],
|
||||||
|
depNum,
|
||||||
|
keys,
|
||||||
|
nodeFunc,
|
||||||
|
nodesMap: new Map(),
|
||||||
|
updateArr: [],
|
||||||
|
didUnmountFuncs: new Map(),
|
||||||
|
willUnmountFuncs: new Map(),
|
||||||
|
savedContextNodes: getSavedCtxNodes(),
|
||||||
|
get _$nodes() {
|
||||||
|
const nodes = [];
|
||||||
|
for (let idx = 0; idx < forNode.array.length; idx++) {
|
||||||
|
nodes.push(...forNode.nodesMap.get(forNode.keys?.[idx] ?? idx)!);
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
addNodeFunc(forNode, nodeFunc);
|
||||||
|
return forNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief To be called immediately after the constructor
|
||||||
|
* @param forNode
|
||||||
|
* @param nodeFunc
|
||||||
|
*/
|
||||||
|
function addNodeFunc<T>(forNode: ForNode<T>, nodeFunc: (item: T, idx: number, updateArr: any[]) => VNode[]) {
|
||||||
|
forNode.array.forEach((item, idx) => {
|
||||||
|
startUnmountScope();
|
||||||
|
const key = forNode.keys?.[idx] ?? idx;
|
||||||
|
const nodes = nodeFunc(item, idx, forNode.updateArr);
|
||||||
|
forNode.nodesMap.set(key, nodes);
|
||||||
|
setUnmountMap(forNode, key);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- For nested ForNode, the whole strategy is just like EnvStore
|
||||||
|
// we use array of function array to create "environment", popping and pushing
|
||||||
|
addWillUnmount(forNode, () => runLifecycleMap(forNode.willUnmountFuncs));
|
||||||
|
addDidUnmount(forNode, () => runLifecycleMap(forNode.didUnmountFuncs));
|
||||||
|
}
|
||||||
|
|
||||||
|
function runLifecycleMap<T>(map: Map<number, any[]>, key?: number) {
|
||||||
|
if (!map || map.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof key === 'number') {
|
||||||
|
const funcs = map.get(key);
|
||||||
|
if (!funcs) return;
|
||||||
|
for (let i = 0; i < funcs.length; i++) funcs[i]?.();
|
||||||
|
map.delete(key);
|
||||||
|
} else {
|
||||||
|
map.forEach(funcs => {
|
||||||
|
for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
|
||||||
|
});
|
||||||
|
map.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the unmount map by getting the last unmount map from the global store
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
function setUnmountMap<T>(forNode: ForNode<T>, key: number) {
|
||||||
|
const [willUnmountMap, didUnmountMap] = endUnmountScope();
|
||||||
|
if (willUnmountMap && willUnmountMap.length > 0) {
|
||||||
|
if (!forNode.willUnmountFuncs) forNode.willUnmountFuncs = new Map();
|
||||||
|
forNode.willUnmountFuncs.set(key, willUnmountMap);
|
||||||
|
}
|
||||||
|
if (didUnmountMap && didUnmountMap.length > 0) {
|
||||||
|
if (!forNode.didUnmountFuncs) forNode.didUnmountFuncs = new Map();
|
||||||
|
forNode.didUnmountFuncs.set(key, didUnmountMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Non-array update function, invoke children's update function
|
||||||
|
* @param changed
|
||||||
|
*/
|
||||||
|
export function updateForChildren<T>(forNode: ForNode<T>, changed: number) {
|
||||||
|
// ---- e.g. this.depNum -> 1110 changed-> 1010
|
||||||
|
// ~this.depNum & changed -> ~1110 & 1010 -> 0000
|
||||||
|
// no update because depNum contains all the changed
|
||||||
|
// ---- e.g. this.depNum -> 1110 changed-> 1101
|
||||||
|
// ~this.depNum & changed -> ~1110 & 1101 -> 000f1
|
||||||
|
// update because depNum doesn't contain all the changed
|
||||||
|
if (!(~forNode.depNum & changed)) return;
|
||||||
|
for (let idx = 0; idx < forNode.array.length; idx++) {
|
||||||
|
updateItem(forNode, idx, forNode.array, changed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update the view related to one item in the array
|
||||||
|
* @param forNode - The ForNode
|
||||||
|
* @param idx - The index of the item in the array
|
||||||
|
* @param array - The array of items
|
||||||
|
* @param changed - The changed bit
|
||||||
|
*/
|
||||||
|
function updateItem<T>(forNode: ForNode<T>, idx: number, array: T[], changed?: number) {
|
||||||
|
// ---- The update function of ForNode's childNodes is stored in the first child node
|
||||||
|
forNode.updateArr[idx]?.(changed ?? forNode.depNum, array[idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Array-related update function
|
||||||
|
*/
|
||||||
|
export function updateForNode<T>(forNode: ForNode<T>, newArray: T[], newKeys: number[]) {
|
||||||
|
if (newKeys) {
|
||||||
|
updateWithKey(forNode, newArray, newKeys);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateWithOutKey(forNode, newArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Shortcut to generate new nodes with idx and key
|
||||||
|
*/
|
||||||
|
function getNewNodes<T>(forNode: ForNode<T>, idx: number, key: number, array: T[], updateArr?: any[]) {
|
||||||
|
startUnmountScope();
|
||||||
|
const nodes = geneNewNodesInCtx(forNode, () => forNode.nodeFunc(array[idx], idx, updateArr ?? forNode.updateArr));
|
||||||
|
setUnmountMap(forNode, key);
|
||||||
|
forNode.nodesMap.set(key, nodes);
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove nodes from parentEl and run willUnmount and didUnmount
|
||||||
|
* @param nodes
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
function removeNodes<T>(forNode: ForNode<T>, nodes: InulaNode[], key: number) {
|
||||||
|
runLifecycleMap(forNode.willUnmountFuncs, key);
|
||||||
|
removeMutableNodes(forNode, nodes);
|
||||||
|
runLifecycleMap(forNode.didUnmountFuncs, key);
|
||||||
|
forNode.nodesMap.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update the nodes without keys
|
||||||
|
* @param newArray
|
||||||
|
*/
|
||||||
|
function updateWithOutKey<T>(forNode: ForNode<T>, newArray: T[]) {
|
||||||
|
const preLength = forNode.array.length;
|
||||||
|
const currLength = newArray.length;
|
||||||
|
|
||||||
|
if (preLength === currLength) {
|
||||||
|
// ---- If the length is the same, we only need to update the nodes
|
||||||
|
for (let idx = 0; idx < forNode.array.length; idx++) {
|
||||||
|
updateItem(forNode, idx, newArray);
|
||||||
|
}
|
||||||
|
forNode.array = [...newArray];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parentEl = forNode._$parentEl!;
|
||||||
|
// ---- If the new array is longer, add new nodes directly
|
||||||
|
if (preLength < currLength) {
|
||||||
|
let flowIndex = getFlowIndexFromNodes(parentEl._$nodes, forNode);
|
||||||
|
// ---- Calling parentEl.childNodes.length is time-consuming,
|
||||||
|
// so we use a length variable to store the length
|
||||||
|
const length = parentEl.childNodes.length;
|
||||||
|
for (let idx = 0; idx < currLength; idx++) {
|
||||||
|
if (idx < preLength) {
|
||||||
|
flowIndex += getFlowIndexFromNodes(forNode.nodesMap.get(idx)!);
|
||||||
|
updateItem(forNode, idx, newArray);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const newNodes = getNewNodes(forNode, idx, idx, newArray);
|
||||||
|
appendNodesWithIndex(newNodes, parentEl, flowIndex, length);
|
||||||
|
}
|
||||||
|
runDidMount();
|
||||||
|
forNode.array = [...newArray];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Update the nodes first
|
||||||
|
for (let idx = 0; idx < currLength; idx++) {
|
||||||
|
updateItem(forNode, idx, newArray);
|
||||||
|
}
|
||||||
|
// ---- If the new array is shorter, remove the extra nodes
|
||||||
|
for (let idx = currLength; idx < preLength; idx++) {
|
||||||
|
const nodes = forNode.nodesMap.get(idx);
|
||||||
|
removeNodes(forNode, nodes!, idx);
|
||||||
|
}
|
||||||
|
forNode.updateArr.splice(currLength, preLength - currLength);
|
||||||
|
forNode.array = [...newArray];
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayEqual<T>(arr1: T[], arr2: T[]) {
|
||||||
|
if (arr1.length !== arr2.length) return false;
|
||||||
|
return arr1.every((item, idx) => item === arr2[idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update the nodes with keys
|
||||||
|
* @param newArray
|
||||||
|
* @param newKeys
|
||||||
|
*/
|
||||||
|
function updateWithKey<T>(forNode: ForNode<T>, newArray: T[], newKeys: number[]) {
|
||||||
|
if (newKeys.length !== new Set(newKeys).size) {
|
||||||
|
throw new Error('Inula-Next: Duplicate keys in for loop are not allowed');
|
||||||
|
}
|
||||||
|
const prevKeys = forNode.keys;
|
||||||
|
forNode.keys = newKeys;
|
||||||
|
|
||||||
|
if (arrayEqual(prevKeys, newKeys)) {
|
||||||
|
// ---- If the keys are the same, we only need to update the nodes
|
||||||
|
for (let idx = 0; idx < newArray.length; idx++) {
|
||||||
|
updateItem(forNode, idx, newArray);
|
||||||
|
}
|
||||||
|
forNode.array = [...newArray];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentEl = forNode._$parentEl!;
|
||||||
|
|
||||||
|
// ---- No nodes after, delete all nodes
|
||||||
|
if (newKeys.length === 0) {
|
||||||
|
const parentNodes = parentEl._$nodes ?? [];
|
||||||
|
if (parentNodes.length === 1 && parentNodes[0] === forNode) {
|
||||||
|
// ---- ForNode is the only node in the parent node
|
||||||
|
// Frequently used in real life scenarios because we tend to always wrap for with a div element,
|
||||||
|
// so we optimize it here
|
||||||
|
runLifecycleMap(forNode.willUnmountFuncs);
|
||||||
|
parentEl.innerHTML = '';
|
||||||
|
runLifecycleMap(forNode.didUnmountFuncs);
|
||||||
|
} else {
|
||||||
|
for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
|
||||||
|
const prevKey = prevKeys[prevIdx];
|
||||||
|
removeNodes(forNode, forNode.nodesMap.get(prevKey)!, prevKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
forNode.nodesMap.clear();
|
||||||
|
forNode.updateArr = [];
|
||||||
|
forNode.array = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Record how many nodes are before this ForNode with the same parentNode
|
||||||
|
const flowIndex = getFlowIndexFromNodes(parentEl._$nodes, forNode);
|
||||||
|
|
||||||
|
// ---- No nodes before, append all nodes
|
||||||
|
if (prevKeys.length === 0) {
|
||||||
|
const nextSibling = parentEl.childNodes[flowIndex];
|
||||||
|
for (let idx = 0; idx < newKeys.length; idx++) {
|
||||||
|
const newNodes = getNewNodes(forNode, idx, newKeys[idx], newArray);
|
||||||
|
appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
||||||
|
}
|
||||||
|
runDidMount();
|
||||||
|
forNode.array = [...newArray];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shuffleKeys: number[] = [];
|
||||||
|
const newUpdateArr: any[] = [];
|
||||||
|
|
||||||
|
// ---- 1. Delete the nodes that are no longer in the array
|
||||||
|
for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
|
||||||
|
const prevKey = prevKeys[prevIdx];
|
||||||
|
if (forNode.keys.includes(prevKey)) {
|
||||||
|
shuffleKeys.push(prevKey);
|
||||||
|
newUpdateArr.push(forNode.updateArr[prevIdx]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
removeNodes(forNode, forNode.nodesMap.get(prevKey)!, prevKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 2. Add the nodes that are not in the array but in the new array
|
||||||
|
// ---- Calling parentEl.childNodes.length is time-consuming,
|
||||||
|
// so we use a length variable to store the length
|
||||||
|
let length = parentEl.childNodes.length;
|
||||||
|
let newFlowIndex = flowIndex;
|
||||||
|
for (let idx = 0; idx < forNode.keys.length; idx++) {
|
||||||
|
const key = forNode.keys[idx];
|
||||||
|
const prevIdx = shuffleKeys.indexOf(key);
|
||||||
|
if (prevIdx !== -1) {
|
||||||
|
// ---- These nodes are already in the parentEl,
|
||||||
|
// and we need to keep track of their flowIndex
|
||||||
|
newFlowIndex += getFlowIndexFromNodes(forNode.nodesMap.get(key)!);
|
||||||
|
newUpdateArr[prevIdx]?.(forNode.depNum, newArray[idx]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// ---- Insert updateArr first because in getNewNode the updateFunc will replace this null
|
||||||
|
newUpdateArr.splice(idx, 0, null);
|
||||||
|
const newNodes = getNewNodes(forNode, idx, key, newArray);
|
||||||
|
// ---- Add the new nodes
|
||||||
|
shuffleKeys.splice(idx, 0, key);
|
||||||
|
|
||||||
|
const count = appendNodesWithIndex(newNodes, parentEl, newFlowIndex, length);
|
||||||
|
newFlowIndex += count;
|
||||||
|
length += count;
|
||||||
|
}
|
||||||
|
runDidMount();
|
||||||
|
|
||||||
|
// ---- After adding and deleting, the only thing left is to reorder the nodes,
|
||||||
|
// but if the keys are the same, we don't need to reorder
|
||||||
|
if (arrayEqual(forNode.keys, shuffleKeys)) {
|
||||||
|
forNode.array = [...newArray];
|
||||||
|
forNode.updateArr = newUpdateArr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newFlowIndex = flowIndex;
|
||||||
|
const bufferNodes = new Map();
|
||||||
|
// ---- 3. Replace the nodes in the same position using Fisher-Yates shuffle algorithm
|
||||||
|
for (let idx = 0; idx < forNode.keys.length; idx++) {
|
||||||
|
const key = forNode.keys[idx];
|
||||||
|
const prevIdx = shuffleKeys.indexOf(key);
|
||||||
|
|
||||||
|
const bufferedNode = bufferNodes.get(key);
|
||||||
|
if (bufferedNode) {
|
||||||
|
// ---- We need to add the flowIndex of the bufferedNode,
|
||||||
|
// because the bufferedNode is in the parentEl and the new position is ahead of the previous position
|
||||||
|
const bufferedFlowIndex = getFlowIndexFromNodes(bufferedNode);
|
||||||
|
const lastEl = toEls(bufferedNode).pop()!;
|
||||||
|
const nextSibling = parentEl.childNodes[newFlowIndex + bufferedFlowIndex];
|
||||||
|
if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
|
||||||
|
// ---- If the node is buffered, we need to add it to the parentEl
|
||||||
|
insertNodesBefore(bufferedNode, parentEl, nextSibling);
|
||||||
|
}
|
||||||
|
// ---- So the added length is the length of the bufferedNode
|
||||||
|
newFlowIndex += bufferedFlowIndex;
|
||||||
|
// TODO: ?? delete bufferNodes[idx];
|
||||||
|
} else if (prevIdx === idx) {
|
||||||
|
// ---- If the node is in the same position, we don't need to do anything
|
||||||
|
newFlowIndex += getFlowIndexFromNodes(forNode.nodesMap.get(key)!);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// ---- If the node is not in the same position, we need to buffer it
|
||||||
|
// We buffer the node of the previous position, and then replace it with the node of the current position
|
||||||
|
const prevKey = shuffleKeys[idx];
|
||||||
|
bufferNodes.set(prevKey, forNode.nodesMap.get(prevKey)!);
|
||||||
|
// ---- Length would never change, and the last will always be in the same position,
|
||||||
|
// so it'll always be insertBefore instead of appendChild
|
||||||
|
const childNodes = forNode.nodesMap.get(key)!;
|
||||||
|
const lastEl = toEls(childNodes).pop()!;
|
||||||
|
const nextSibling = parentEl.childNodes[newFlowIndex];
|
||||||
|
if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
|
||||||
|
newFlowIndex += insertNodesBefore(childNodes, parentEl, nextSibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---- Swap the keys
|
||||||
|
const tempKey = shuffleKeys[idx];
|
||||||
|
shuffleKeys[idx] = shuffleKeys[prevIdx];
|
||||||
|
shuffleKeys[prevIdx] = tempKey;
|
||||||
|
const tempUpdateFunc = newUpdateArr[idx];
|
||||||
|
newUpdateArr[idx] = newUpdateArr[prevIdx];
|
||||||
|
newUpdateArr[prevIdx] = tempUpdateFunc;
|
||||||
|
}
|
||||||
|
forNode.array = [...newArray];
|
||||||
|
forNode.updateArr = newUpdateArr;
|
||||||
|
}
|
|
@ -1,71 +0,0 @@
|
||||||
import { DLNode } from '../DLNode';
|
|
||||||
import { DLStore } from '../store';
|
|
||||||
|
|
||||||
export class MutableNode extends DLNode {
|
|
||||||
/**
|
|
||||||
* @brief Mutable node is a node that this._$nodes can be changed, things need to pay attention:
|
|
||||||
* 1. The environment of the new nodes should be the same as the old nodes
|
|
||||||
* 2. The new nodes should be added to the parentEl
|
|
||||||
* 3. The old nodes should be removed from the parentEl
|
|
||||||
* @param type
|
|
||||||
*/
|
|
||||||
constructor(type) {
|
|
||||||
super(type);
|
|
||||||
// ---- Save the current environment nodes, must be a new reference
|
|
||||||
if (DLStore.global.DLEnvStore && DLStore.global.DLEnvStore.currentEnvNodes.length > 0) {
|
|
||||||
this.savedEnvNodes = [...DLStore.global.DLEnvStore.currentEnvNodes];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Initialize the new nodes, add parentEl to all nodes
|
|
||||||
* @param nodes
|
|
||||||
*/
|
|
||||||
initNewNodes(nodes) {
|
|
||||||
// ---- Add parentEl to all nodes
|
|
||||||
DLNode.addParentEl(nodes, this._$parentEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Generate new nodes in the saved environment
|
|
||||||
* @param newNodesFunc
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
geneNewNodesInEnv(newNodesFunc) {
|
|
||||||
if (!this.savedEnvNodes) {
|
|
||||||
// ---- No saved environment, just generate new nodes
|
|
||||||
const newNodes = newNodesFunc();
|
|
||||||
// ---- Only for IfNode's same condition return
|
|
||||||
// ---- Initialize the new nodes
|
|
||||||
this.initNewNodes(newNodes);
|
|
||||||
return newNodes;
|
|
||||||
}
|
|
||||||
// ---- Save the current environment nodes
|
|
||||||
const currentEnvNodes = DLStore.global.DLEnvStore.currentEnvNodes;
|
|
||||||
// ---- Replace the saved environment nodes
|
|
||||||
DLStore.global.DLEnvStore.replaceEnvNodes(this.savedEnvNodes);
|
|
||||||
const newNodes = newNodesFunc();
|
|
||||||
// ---- Retrieve the current environment nodes
|
|
||||||
DLStore.global.DLEnvStore.replaceEnvNodes(currentEnvNodes);
|
|
||||||
// ---- Only for IfNode's same condition return
|
|
||||||
// ---- Initialize the new nodes
|
|
||||||
this.initNewNodes(newNodes);
|
|
||||||
return newNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
initUnmountStore() {
|
|
||||||
DLStore.global.WillUnmountStore.push([]);
|
|
||||||
DLStore.global.DidUnmountStore.push([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Remove nodes from parentEl and run willUnmount and didUnmount
|
|
||||||
* @param nodes
|
|
||||||
* @param removeEl Only remove outermost element
|
|
||||||
*/
|
|
||||||
removeNodes(nodes) {
|
|
||||||
DLNode.loopShallowEls(nodes, node => {
|
|
||||||
this._$parentEl.removeChild(node);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
import { DLNodeType } from '../DLNode';
|
|
||||||
import { FlatNode } from './FlatNode';
|
|
||||||
import { EnvNode } from '../EnvNode';
|
|
||||||
|
|
||||||
export class TryNode extends FlatNode {
|
|
||||||
constructor(tryFunc, catchFunc) {
|
|
||||||
super(DLNodeType.Try);
|
|
||||||
this.tryFunc = tryFunc;
|
|
||||||
const catchable = this.getCatchable(catchFunc);
|
|
||||||
this.envNode = new EnvNode({ _$catchable: catchable });
|
|
||||||
const nodes = tryFunc(this.setUpdateFunc.bind(this), catchable) ?? [];
|
|
||||||
this.envNode.initNodes(nodes);
|
|
||||||
this._$nodes = nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
update(changed) {
|
|
||||||
this.updateFunc?.(changed);
|
|
||||||
}
|
|
||||||
|
|
||||||
setUpdateFunc(updateFunc) {
|
|
||||||
this.updateFunc = updateFunc;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCatchable(catchFunc) {
|
|
||||||
return callback =>
|
|
||||||
(...args) => {
|
|
||||||
try {
|
|
||||||
return callback(...args);
|
|
||||||
} catch (e) {
|
|
||||||
// ---- Run it in next tick to make sure when error occurs before
|
|
||||||
// didMount, this._$parentEl is not null
|
|
||||||
Promise.resolve().then(() => {
|
|
||||||
const nodes = this.geneNewNodesInEnv(() => catchFunc(this.setUpdateFunc.bind(this), e));
|
|
||||||
this._$nodes && this.removeNodes(this._$nodes);
|
|
||||||
const parentEl = this._$parentEl;
|
|
||||||
const flowIndex = FlatNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
|
||||||
const nextSibling = parentEl.childNodes[flowIndex];
|
|
||||||
FlatNode.appendNodesWithSibling(nodes, parentEl, nextSibling);
|
|
||||||
FlatNode.runDidMount();
|
|
||||||
this._$nodes = nodes;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { MutableNode, VNode, ScopedLifecycle, InulaNode, ContextNode } from '../types';
|
||||||
|
import { endUnmountScope, startUnmountScope } from '../lifecycle';
|
||||||
|
import { getContextNodeMap, replaceContext } from '../ContextNode';
|
||||||
|
import { addParentEl, loopShallowEls } from '../InulaNode';
|
||||||
|
|
||||||
|
export function setUnmountFuncs(node: MutableNode) {
|
||||||
|
// pop will not be undefined,cause we push empty array when create node
|
||||||
|
const [willUnmountFuncs, didUnmountFuncs] = endUnmountScope();
|
||||||
|
node.willUnmountFuncs = willUnmountFuncs!;
|
||||||
|
node.didUnmountFuncs = didUnmountFuncs!;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runLifeCycle(fn: ScopedLifecycle) {
|
||||||
|
for (let i = 0; i < fn.length; i++) {
|
||||||
|
fn[i]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeNodesWithUnmount(node: MutableNode, children: InulaNode[]) {
|
||||||
|
runLifeCycle(node.willUnmountFuncs);
|
||||||
|
removeNodes(node, children);
|
||||||
|
runLifeCycle(node.didUnmountFuncs);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function geneNewNodesInEnvWithUnmount(node: MutableNode, newNodesFunc: () => VNode[]) {
|
||||||
|
startUnmountScope();
|
||||||
|
const nodes = geneNewNodesInCtx(node, newNodesFunc);
|
||||||
|
setUnmountFuncs(node);
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
export function getSavedCtxNodes(): Map<symbol, ContextNode<any>> | null {
|
||||||
|
const contextNodeMap = getContextNodeMap();
|
||||||
|
if (contextNodeMap) {
|
||||||
|
return new Map([...contextNodeMap]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @brief Initialize the new nodes, add parentEl to all nodes
|
||||||
|
*/
|
||||||
|
function initNewNodes(node: VNode, children: Array<InulaNode>) {
|
||||||
|
// ---- Add parentEl to all children
|
||||||
|
addParentEl(children, node._$parentEl!);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @brief Generate new nodes in the saved context
|
||||||
|
* @param node
|
||||||
|
* @param newNodesFunc
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function geneNewNodesInCtx(node: MutableNode<any>, newNodesFunc: () => Array<InulaNode>) {
|
||||||
|
if (!node.savedContextNodes) {
|
||||||
|
// ---- No saved context, just generate new nodes
|
||||||
|
const newNodes = newNodesFunc();
|
||||||
|
// ---- Only for IfNode's same condition return
|
||||||
|
// ---- Initialize the new nodes
|
||||||
|
initNewNodes(node, newNodes);
|
||||||
|
return newNodes;
|
||||||
|
}
|
||||||
|
// ---- Save the current context nodes
|
||||||
|
const currentContextNodes = getContextNodeMap()!;
|
||||||
|
// ---- Replace the saved context nodes
|
||||||
|
replaceContext(node.savedContextNodes);
|
||||||
|
const newNodes = newNodesFunc();
|
||||||
|
// ---- Retrieve the current context nodes
|
||||||
|
replaceContext(currentContextNodes);
|
||||||
|
// ---- Only for IfNode's same condition return
|
||||||
|
// ---- Initialize the new nodes
|
||||||
|
initNewNodes(node, newNodes);
|
||||||
|
return newNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove nodes from parentEl and run willUnmount and didUnmount
|
||||||
|
*/
|
||||||
|
export function removeNodes(node: VNode, children: InulaNode[]) {
|
||||||
|
loopShallowEls(children, dom => {
|
||||||
|
node._$parentEl!.removeChild(dom);
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,48 +0,0 @@
|
||||||
import { DLNode } from './DLNode';
|
|
||||||
import { insertNode } from './HTMLNode';
|
|
||||||
export class PropView {
|
|
||||||
propViewFunc;
|
|
||||||
dlUpdateFunc = new Set();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief PropView constructor, accept a function that returns a list of DLNode
|
|
||||||
* @param propViewFunc - A function that when called, collects and returns an array of DLNode instances
|
|
||||||
*/
|
|
||||||
constructor(propViewFunc) {
|
|
||||||
this.propViewFunc = propViewFunc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Build the prop view by calling the propViewFunc and add every single instance of the returned DLNode to dlUpdateNodes
|
|
||||||
* @returns An array of DLNode instances returned by propViewFunc
|
|
||||||
*/
|
|
||||||
build() {
|
|
||||||
let update;
|
|
||||||
const addUpdate = updateFunc => {
|
|
||||||
update = updateFunc;
|
|
||||||
this.dlUpdateFunc.add(updateFunc);
|
|
||||||
};
|
|
||||||
const newNodes = this.propViewFunc(addUpdate);
|
|
||||||
if (newNodes.length === 0) return [];
|
|
||||||
if (update) {
|
|
||||||
// Remove the updateNode from dlUpdateNodes when it unmounts
|
|
||||||
DLNode.addWillUnmount(newNodes[0], this.dlUpdateFunc.delete.bind(this.dlUpdateFunc, update));
|
|
||||||
}
|
|
||||||
|
|
||||||
return newNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update every node in dlUpdateNodes
|
|
||||||
* @param changed - A parameter indicating what changed to trigger the update
|
|
||||||
*/
|
|
||||||
update(...args) {
|
|
||||||
this.dlUpdateFunc.forEach(update => {
|
|
||||||
update(...args);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function insertChildren(el, propView) {
|
|
||||||
insertNode(el, { _$nodes: propView.build(), _$dlNodeType: 7 }, 0);
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { DLNode, DLNodeType } from './DLNode';
|
|
||||||
import { cached } from './store';
|
|
||||||
|
|
||||||
export class SnippetNode extends DLNode {
|
|
||||||
constructor(depsArr) {
|
|
||||||
super(DLNodeType.Snippet);
|
|
||||||
this.depsArr = depsArr;
|
|
||||||
}
|
|
||||||
|
|
||||||
cached(deps, changed) {
|
|
||||||
if (!deps || !deps.length) return false;
|
|
||||||
const idx = Math.log2(changed);
|
|
||||||
const prevDeps = this.depsArr[idx];
|
|
||||||
if (cached(deps, prevDeps)) return true;
|
|
||||||
this.depsArr[idx] = deps;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { DLStore, cached } from './store';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Shorten document.createTextNode
|
|
||||||
* @param value
|
|
||||||
* @returns Text
|
|
||||||
*/
|
|
||||||
export function createTextNode(value, deps) {
|
|
||||||
const node = DLStore.document.createTextNode(value);
|
|
||||||
node.$$deps = deps;
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update text node and check if the value is changed
|
|
||||||
* @param node
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function updateText(node, valueFunc, deps) {
|
|
||||||
if (cached(deps, node.$$deps)) return;
|
|
||||||
const value = valueFunc();
|
|
||||||
node.textContent = value;
|
|
||||||
node.$$deps = deps;
|
|
||||||
}
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* @brief Shallowly compare the deps with the previous deps
|
||||||
|
* @param deps
|
||||||
|
* @param prevDeps
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function equal(deps: any[], prevDeps: any[]) {
|
||||||
|
if (!prevDeps || deps.length !== prevDeps.length) return false;
|
||||||
|
return deps.every((dep, i) => !(dep instanceof Object) && prevDeps[i] === dep);
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
export * from './types/index';
|
|
|
@ -1,62 +0,0 @@
|
||||||
import { DLNode } from './DLNode';
|
|
||||||
import { insertNode } from './HTMLNode';
|
|
||||||
|
|
||||||
export * from './HTMLNode';
|
|
||||||
export * from './CompNode';
|
|
||||||
export * from './EnvNode';
|
|
||||||
export * from './TextNode';
|
|
||||||
export * from './PropView';
|
|
||||||
export * from './SnippetNode';
|
|
||||||
export * from './MutableNode/ForNode';
|
|
||||||
export * from './MutableNode/ExpNode';
|
|
||||||
export * from './MutableNode/CondNode';
|
|
||||||
export * from './MutableNode/TryNode';
|
|
||||||
|
|
||||||
import { DLStore } from './store';
|
|
||||||
|
|
||||||
export { setGlobal, setDocument } from './store';
|
|
||||||
|
|
||||||
function initStore() {
|
|
||||||
// Declare a global variable to store willUnmount functions
|
|
||||||
DLStore.global.WillUnmountStore = [];
|
|
||||||
// Declare a global variable to store didUnmount functions
|
|
||||||
DLStore.global.DidUnmountStore = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Render the DL class to the element
|
|
||||||
* @param {typeof import('./CompNode').CompNode} Comp
|
|
||||||
* @param {HTMLElement | string} idOrEl
|
|
||||||
*/
|
|
||||||
export function render(Comp, idOrEl) {
|
|
||||||
let el = idOrEl;
|
|
||||||
if (typeof idOrEl === 'string') {
|
|
||||||
const elFound = DLStore.document.getElementById(idOrEl);
|
|
||||||
if (elFound) el = elFound;
|
|
||||||
else {
|
|
||||||
throw new Error(`DLight: Element with id ${idOrEl} not found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
initStore();
|
|
||||||
el.innerHTML = '';
|
|
||||||
const dlNode = new Comp();
|
|
||||||
dlNode._$init();
|
|
||||||
insertNode(el, dlNode, 0);
|
|
||||||
DLNode.runDidMount();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function manual(callback, _deps) {
|
|
||||||
return callback();
|
|
||||||
}
|
|
||||||
export function escape(arg) {
|
|
||||||
return arg;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const $ = escape;
|
|
||||||
export const required = null;
|
|
||||||
|
|
||||||
export function use() {
|
|
||||||
console.error(
|
|
||||||
'DLight: use() is not supported be called directly. You can only assign `use(model)` to a dlight class property. Any other expressions are not allowed.'
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,199 @@
|
||||||
|
import { runDidMount } from './lifecycle';
|
||||||
|
import { insertNode } from './renderer/dom';
|
||||||
|
import { equal } from './equal';
|
||||||
|
import { constructComp, createCompNode, updateCompNode } from './CompNode';
|
||||||
|
import { constructHook, createHookNode } from './HookNode';
|
||||||
|
import { CompNode, VNode, InulaHTMLNode, HookNode, ChildrenNode } from './types';
|
||||||
|
import { createContextNode, updateContextNode } from './ContextNode';
|
||||||
|
import { InulaNodeType } from '@openinula/next-shared';
|
||||||
|
import { createChildrenNode, updateChildrenNode } from './ChildrenNode';
|
||||||
|
import { createForNode, updateForChildren, updateForNode } from './MutableNode/ForNode';
|
||||||
|
import { createExpNode, updateExpNode } from './MutableNode/ExpNode';
|
||||||
|
import { createCondNode, updateCondChildren, updateCondNode } from './MutableNode/CondNode';
|
||||||
|
|
||||||
|
export * from './renderer/dom';
|
||||||
|
export * from './CompNode';
|
||||||
|
export * from './ContextNode';
|
||||||
|
export * from './MutableNode/ForNode';
|
||||||
|
export * from './MutableNode/ExpNode';
|
||||||
|
export * from './MutableNode/CondNode';
|
||||||
|
|
||||||
|
export type FunctionComponent = (props: Record<PropertyKey, unknown>) => CompNode | HookNode;
|
||||||
|
|
||||||
|
export function render(compFn: FunctionComponent, container: HTMLElement): void {
|
||||||
|
if (container == null) {
|
||||||
|
throw new Error('Render target is empty. Please provide a valid DOM element.');
|
||||||
|
}
|
||||||
|
container.innerHTML = '';
|
||||||
|
const node = Comp(compFn);
|
||||||
|
insertNode(container as InulaHTMLNode, node, 0);
|
||||||
|
runDidMount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// export function unmount(container: InulaHTMLNode): void {
|
||||||
|
// const node = container.firstChild;
|
||||||
|
// if (node) {
|
||||||
|
// removeNode(node);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function untrack<V>(callback: () => V): V {
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
export let currentComp: CompNode | HookNode | null = null;
|
||||||
|
|
||||||
|
export function inMount(): boolean {
|
||||||
|
return !!currentComp;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompUpdater {
|
||||||
|
updateState: (bit: number) => void;
|
||||||
|
updateProp: (propName: string, newValue: unknown) => void;
|
||||||
|
getUpdateViews: () => [HTMLElement[], (bit: number) => HTMLElement[]];
|
||||||
|
updateDerived: (newValue: unknown, bit: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Comp(compFn: FunctionComponent, props: Record<string, unknown> = {}): CompNode {
|
||||||
|
return mountNode(() => createCompNode(), compFn, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountNode<T extends CompNode | HookNode>(
|
||||||
|
ctor: () => T,
|
||||||
|
compFn: FunctionComponent,
|
||||||
|
props: Record<string, unknown>
|
||||||
|
): T {
|
||||||
|
const compNode = ctor();
|
||||||
|
const prevNode = currentComp;
|
||||||
|
try {
|
||||||
|
currentComp = compNode;
|
||||||
|
compFn(props);
|
||||||
|
// eslint-disable-next-line no-useless-catch
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
currentComp = prevNode;
|
||||||
|
}
|
||||||
|
return compNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createComponent(compUpdater: CompUpdater): CompNode {
|
||||||
|
if (!currentComp || currentComp.__type !== InulaNodeType.Comp) {
|
||||||
|
throw new Error('Should not call createComponent outside the component function');
|
||||||
|
}
|
||||||
|
constructComp(currentComp, compUpdater);
|
||||||
|
return currentComp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notCached(node: VNode, cacheId: string, cacheValues: unknown[]): boolean {
|
||||||
|
if (!cacheValues || !cacheValues.length) return false;
|
||||||
|
if (!node.$nonkeyedCache) {
|
||||||
|
node.$nonkeyedCache = {};
|
||||||
|
}
|
||||||
|
if (!equal(cacheValues, node.$nonkeyedCache[cacheId])) {
|
||||||
|
node.$nonkeyedCache[cacheId] = cacheValues;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function didMount(fn: () => void) {
|
||||||
|
throw new Error('lifecycle should be compiled, check the babel plugin');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function willUnmount(fn: () => void) {
|
||||||
|
throw new Error('lifecycle should be compiled, check the babel plugin');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function didUnMount(fn: () => void) {
|
||||||
|
throw new Error('lifecycle should be compiled, check the babel plugin');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHook(hookFn: (props: Record<string, unknown>) => HookNode, params: unknown[], bitMap: number) {
|
||||||
|
if (currentComp) {
|
||||||
|
const props = params.reduce<Record<string, unknown>>((obj, val, idx) => ({ ...obj, [`p${idx}`]: val }), {});
|
||||||
|
// if there is the currentComp means we are mounting the component tree
|
||||||
|
return mountNode(() => createHookNode(currentComp!, bitMap), hookFn, props);
|
||||||
|
}
|
||||||
|
throw new Error('useHook must be called within a component');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHook(compUpdater: CompUpdater): HookNode {
|
||||||
|
if (!currentComp || currentComp.__type !== InulaNodeType.Hook) {
|
||||||
|
throw new Error('Should not call createComponent outside the component function');
|
||||||
|
}
|
||||||
|
constructHook(currentComp, compUpdater);
|
||||||
|
return currentComp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runOnce(fn: () => void): void {
|
||||||
|
if (currentComp) {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createNode(type: InulaNodeType, ...args: unknown[]): VNode | ChildrenNode {
|
||||||
|
switch (type) {
|
||||||
|
case InulaNodeType.Context:
|
||||||
|
return createContextNode(...(args as Parameters<typeof createContextNode>));
|
||||||
|
case InulaNodeType.Children:
|
||||||
|
return createChildrenNode(...(args as Parameters<typeof createChildrenNode>));
|
||||||
|
case InulaNodeType.Comp:
|
||||||
|
return createCompNode(...(args as Parameters<typeof createCompNode>));
|
||||||
|
case InulaNodeType.Hook:
|
||||||
|
return createHookNode(...(args as Parameters<typeof createHookNode>));
|
||||||
|
case InulaNodeType.For:
|
||||||
|
return createForNode(...(args as Parameters<typeof createForNode>));
|
||||||
|
case InulaNodeType.Cond:
|
||||||
|
return createCondNode(...(args as Parameters<typeof createCondNode>));
|
||||||
|
case InulaNodeType.Exp:
|
||||||
|
return createExpNode(...(args as Parameters<typeof createExpNode>));
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported node type: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateNode(...args: unknown[]) {
|
||||||
|
const node = args[0] as VNode;
|
||||||
|
switch (node.__type) {
|
||||||
|
case InulaNodeType.Context:
|
||||||
|
updateContextNode(...(args as Parameters<typeof updateContextNode>));
|
||||||
|
break;
|
||||||
|
case InulaNodeType.Children:
|
||||||
|
updateChildrenNode(...(args as Parameters<typeof updateChildrenNode>));
|
||||||
|
break;
|
||||||
|
case InulaNodeType.For:
|
||||||
|
updateForNode(...(args as Parameters<typeof updateForNode>));
|
||||||
|
break;
|
||||||
|
case InulaNodeType.Cond:
|
||||||
|
updateCondNode(...(args as Parameters<typeof updateCondNode>));
|
||||||
|
break;
|
||||||
|
case InulaNodeType.Exp:
|
||||||
|
updateExpNode(...(args as Parameters<typeof updateExpNode>));
|
||||||
|
break;
|
||||||
|
case InulaNodeType.Comp:
|
||||||
|
case InulaNodeType.Hook:
|
||||||
|
updateCompNode(...(args as Parameters<typeof updateCompNode>));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported node type: ${node.__type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateChildren(...args: unknown[]) {
|
||||||
|
const node = args[0] as VNode;
|
||||||
|
switch (node.__type) {
|
||||||
|
case InulaNodeType.For:
|
||||||
|
updateForChildren(...(args as Parameters<typeof updateForChildren>));
|
||||||
|
break;
|
||||||
|
case InulaNodeType.Cond:
|
||||||
|
updateCondChildren(...(args as Parameters<typeof updateCondChildren>));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported node type: ${node.__type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initContextChildren, createContext, useContext } from './ContextNode';
|
||||||
|
export { initCompNode } from './CompNode';
|
||||||
|
export { emitUpdate } from './HookNode';
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { VNode, ScopedLifecycle } from './types';
|
||||||
|
|
||||||
|
let DidMountStore: ScopedLifecycle;
|
||||||
|
const WillUnmountStore: ScopedLifecycle[] = [];
|
||||||
|
const DidUnmountStore: ScopedLifecycle[] = [];
|
||||||
|
|
||||||
|
export const addWillUnmount = (node: VNode, func: (node: VNode) => void): void => {
|
||||||
|
const willUnmountStore = WillUnmountStore;
|
||||||
|
const currentStore = willUnmountStore[willUnmountStore.length - 1];
|
||||||
|
if (!currentStore) return;
|
||||||
|
currentStore.push(() => func(node));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addDidUnmount = (node: VNode, func: (node: VNode) => void): void => {
|
||||||
|
const didUnmountStore = DidUnmountStore;
|
||||||
|
const currentStore = didUnmountStore[didUnmountStore.length - 1];
|
||||||
|
if (!currentStore) return;
|
||||||
|
currentStore.push(() => func(node));
|
||||||
|
};
|
||||||
|
export const addDidMount = (node: VNode, func: (node: VNode) => void): void => {
|
||||||
|
if (!DidMountStore) DidMountStore = [];
|
||||||
|
DidMountStore.push(() => func(node));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runDidMount = (): void => {
|
||||||
|
const didMountStore = DidMountStore;
|
||||||
|
if (!didMountStore || didMountStore.length === 0) return;
|
||||||
|
for (let i = didMountStore.length - 1; i >= 0; i--) {
|
||||||
|
didMountStore[i]();
|
||||||
|
}
|
||||||
|
DidMountStore = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function startUnmountScope() {
|
||||||
|
WillUnmountStore.push([]);
|
||||||
|
DidUnmountStore.push([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function endUnmountScope() {
|
||||||
|
return [WillUnmountStore.pop(), DidUnmountStore.pop()];
|
||||||
|
}
|
|
@ -0,0 +1,222 @@
|
||||||
|
import { getFlowIndexFromNodes, appendNodesWithIndex, addParentEl } from '../InulaNode';
|
||||||
|
import { equal } from '../equal';
|
||||||
|
import { InulaHTMLNode, TextNode, InulaNode } from '../types';
|
||||||
|
|
||||||
|
const delegatedEvents = new Set<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Shortcut for document.createElement
|
||||||
|
* @param tag
|
||||||
|
* @returns HTMLElement
|
||||||
|
*/
|
||||||
|
export function createElement(tag: string): HTMLElement {
|
||||||
|
return document.createElement(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Shorten document.createTextNode
|
||||||
|
* @param value
|
||||||
|
* @returns Text
|
||||||
|
*/
|
||||||
|
export function createTextNode(value: string, deps?: unknown[]) {
|
||||||
|
const node = document.createTextNode(value) as unknown as TextNode;
|
||||||
|
if (deps) node.deps = deps;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update text node and check if the value is changed
|
||||||
|
* @param node
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export function updateText(node: TextNode, valueFunc: () => string, deps: unknown[]) {
|
||||||
|
if (equal(deps, node.deps)) return;
|
||||||
|
const value = valueFunc();
|
||||||
|
node.textContent = value;
|
||||||
|
node.deps = deps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cache(el: HTMLElement, key: string, deps: any[]): boolean {
|
||||||
|
if (deps.length === 0) return false;
|
||||||
|
const cacheKey = `$${key}`;
|
||||||
|
if (equal(deps, (el as any)[cacheKey])) return true;
|
||||||
|
(el as any)[cacheKey] = deps;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCustomProperty = (name: string): boolean => name.startsWith('--');
|
||||||
|
|
||||||
|
interface StyleObject {
|
||||||
|
[key: string]: string | number | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setStyle(el: InulaHTMLNode, newStyle: CSSStyleDeclaration): void {
|
||||||
|
const style = el.style;
|
||||||
|
const prevStyle = el._prevStyle || {};
|
||||||
|
|
||||||
|
// Remove styles that are no longer present
|
||||||
|
for (const key in prevStyle) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (prevStyle.hasOwnProperty(key) && (newStyle == null || !newStyle.hasOwnProperty(key))) {
|
||||||
|
if (isCustomProperty(key)) {
|
||||||
|
style.removeProperty(key);
|
||||||
|
} else if (key === 'float') {
|
||||||
|
style.cssFloat = '';
|
||||||
|
} else {
|
||||||
|
style[key] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new or changed styles
|
||||||
|
for (const key in newStyle) {
|
||||||
|
const prevValue = prevStyle[key];
|
||||||
|
const newValue = newStyle[key];
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (newStyle.hasOwnProperty(key) && newValue !== prevValue) {
|
||||||
|
if (newValue == null || newValue === '' || typeof newValue === 'boolean') {
|
||||||
|
if (isCustomProperty(key)) {
|
||||||
|
style.removeProperty(key);
|
||||||
|
} else if (key === 'float') {
|
||||||
|
style.cssFloat = '';
|
||||||
|
} else {
|
||||||
|
style[key] = '';
|
||||||
|
}
|
||||||
|
} else if (isCustomProperty(key)) {
|
||||||
|
style.setProperty(key, newValue);
|
||||||
|
} else if (key === 'float') {
|
||||||
|
style.cssFloat = newValue;
|
||||||
|
} else {
|
||||||
|
el.style[key] = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the new style for future comparisons
|
||||||
|
el._prevStyle = { ...newStyle };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Plainly set dataset
|
||||||
|
* @param el
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function setDataset(el: HTMLElement, value: { [key: string]: string }): void {
|
||||||
|
Object.assign(el.dataset, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set HTML property with checking value equality first
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function setHTMLProp(el: HTMLElement, key: string, valueFunc: () => any, deps: any[]): void {
|
||||||
|
// ---- Comparing deps, same value won't trigger
|
||||||
|
// will lead to a bug if the value is set outside of the DLNode
|
||||||
|
// e.g. setHTMLProp(el, "textContent", "value", [])
|
||||||
|
// => el.textContent = "other"
|
||||||
|
// => setHTMLProp(el, "textContent", "value", [])
|
||||||
|
// The value will be set to "other" instead of "value"
|
||||||
|
if (cache(el, key, deps)) return;
|
||||||
|
(el as any)[key] = valueFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Plainly set HTML properties
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function setHTMLProps(el: InulaHTMLNode, value: HTMLAttrsObject): void {
|
||||||
|
Object.entries(value).forEach(([key, v]) => {
|
||||||
|
if (key === 'style') return setStyle(el, v as CSSStyleDeclaration);
|
||||||
|
if (key === 'dataset') return setDataset(el, v as { [key: string]: string });
|
||||||
|
setHTMLProp(el, key, () => v, []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set HTML attribute with checking value equality first
|
||||||
|
* @param el
|
||||||
|
* @param key
|
||||||
|
* @param valueFunc
|
||||||
|
* @param deps
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function setHTMLAttr(el: InulaHTMLNode, key: string, valueFunc: () => string, deps: any[]): void {
|
||||||
|
if (cache(el, key, deps)) return;
|
||||||
|
el.setAttribute(key, valueFunc());
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HTMLAttrsObject {
|
||||||
|
[key: string]: string | number | boolean | object | undefined;
|
||||||
|
style?: CSSStyleDeclaration;
|
||||||
|
dataset?: { [key: string]: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Plainly set HTML attributes
|
||||||
|
* @param el
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function setHTMLAttrs(el: InulaHTMLNode, value: HTMLAttrsObject): void {
|
||||||
|
Object.entries(value).forEach(([key, v]) => {
|
||||||
|
setHTMLAttr(el, key, () => v, []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set memorized event, store the previous event in el[`$on${key}`], if it exists, remove it first
|
||||||
|
* @param el
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function setEvent(el: InulaHTMLNode, key: string, value: EventListener): void {
|
||||||
|
const prevEvent = el[`$on${key}`];
|
||||||
|
if (prevEvent) el.removeEventListener(key, prevEvent);
|
||||||
|
el.addEventListener(key, value);
|
||||||
|
el[`$on${key}`] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventHandler(e: Event): void {
|
||||||
|
const key = `$$${e.type}`;
|
||||||
|
for (const node of e.composedPath()) {
|
||||||
|
if (node[key]) node[key](e);
|
||||||
|
if (e.cancelBubble) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delegateEvent(el: InulaHTMLNode, key: string, value: EventListener): void {
|
||||||
|
if (el[`$$${key}`] === value) return;
|
||||||
|
el[`$$${key}`] = value;
|
||||||
|
if (!delegatedEvents.has(key)) {
|
||||||
|
delegatedEvents.add(key);
|
||||||
|
document.addEventListener(key, eventHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appendNode(el: InulaHTMLNode, child: InulaHTMLNode) {
|
||||||
|
// ---- Set _$nodes
|
||||||
|
if (!el._$nodes) el._$nodes = Array.from(el.childNodes) as InulaHTMLNode[];
|
||||||
|
el._$nodes.push(child);
|
||||||
|
|
||||||
|
el.appendChild(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Insert any DLNode into an element, set the _$nodes and append the element to the element's children
|
||||||
|
* @param el
|
||||||
|
* @param node
|
||||||
|
* @param position
|
||||||
|
*/
|
||||||
|
export function insertNode(el: InulaHTMLNode, node: InulaNode, position: number): void {
|
||||||
|
// ---- Set _$nodes
|
||||||
|
if (!el._$nodes) el._$nodes = Array.from(el.childNodes) as InulaHTMLNode[];
|
||||||
|
el._$nodes.splice(position, 0, node);
|
||||||
|
|
||||||
|
// ---- Insert nodes' elements
|
||||||
|
const flowIdx = getFlowIndexFromNodes(el._$nodes, node);
|
||||||
|
appendNodesWithIndex([node], el, flowIdx);
|
||||||
|
// ---- Set parentEl
|
||||||
|
addParentEl([node], el);
|
||||||
|
}
|
|
@ -1,25 +1,23 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
*
|
*
|
||||||
* openInula is licensed under Mulan PSL v2.
|
* openInula is licensed under Mulan PSL v2.
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
*
|
*
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
*
|
*
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const p = Promise.resolve();
|
const p = Promise.resolve();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule a task to run in the next microtask.
|
* Schedule a task to run in the next microtask.
|
||||||
*
|
*/
|
||||||
* @param {() => void} task
|
export function schedule(task: () => void) {
|
||||||
*/
|
p.then(task);
|
||||||
export function schedule(task) {
|
}
|
||||||
p.then(task);
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
import { Store } from '@openinula/store';
|
|
||||||
|
|
||||||
// ---- Using external Store to store global and document
|
|
||||||
// Because Store is a singleton, it is safe to use it as a global variable
|
|
||||||
// If created in DLight package, different package versions will introduce
|
|
||||||
// multiple Store instances.
|
|
||||||
|
|
||||||
if (!('global' in Store)) {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
Store.global = window;
|
|
||||||
} else if (typeof global !== 'undefined') {
|
|
||||||
Store.global = global;
|
|
||||||
} else {
|
|
||||||
Store.global = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!('document' in Store)) {
|
|
||||||
if (typeof document !== 'undefined') {
|
|
||||||
Store.document = document;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DLStore = { ...Store, delegatedEvents: new Set() };
|
|
||||||
|
|
||||||
export function setGlobal(globalObj) {
|
|
||||||
DLStore.global = globalObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setDocument(customDocument) {
|
|
||||||
DLStore.document = customDocument;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Compare the deps with the previous deps
|
|
||||||
* @param deps
|
|
||||||
* @param prevDeps
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function cached(deps, prevDeps) {
|
|
||||||
if (!prevDeps || deps.length !== prevDeps.length) return false;
|
|
||||||
return deps.every((dep, i) => !(dep instanceof Object) && prevDeps[i] === dep);
|
|
||||||
}
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { InulaNodeType } from '@openinula/next-shared';
|
||||||
|
|
||||||
|
export type Lifecycle = () => void;
|
||||||
|
export type ScopedLifecycle = Lifecycle[];
|
||||||
|
|
||||||
|
export { type Properties as CSSProperties } from 'csstype';
|
||||||
|
|
||||||
|
export type InulaNode = VNode | TextNode | InulaHTMLNode;
|
||||||
|
|
||||||
|
export interface VNode {
|
||||||
|
__type: InulaNodeType;
|
||||||
|
_$nodes: InulaNode[];
|
||||||
|
_$parentEl?: InulaHTMLNode;
|
||||||
|
$nonkeyedCache?: Record<string, unknown[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextNode extends Text {
|
||||||
|
deps: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InulaHTMLNode extends HTMLElement {
|
||||||
|
_$nodes: InulaNode[];
|
||||||
|
_prevStyle?: CSSStyleDeclaration;
|
||||||
|
[key: `$on${string}`]: EventListener
|
||||||
|
[key: `$$${string}`]: EventListener
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComposableNode<Props extends Record<PropertyKey, any> = Record<PropertyKey, any>> extends VNode {
|
||||||
|
__type: InulaNodeType;
|
||||||
|
parent?: ComposableNode;
|
||||||
|
props: Props;
|
||||||
|
cache?: Record<string, any>;
|
||||||
|
_$nodes: InulaHTMLNode[];
|
||||||
|
mounting?: boolean;
|
||||||
|
_$unmounted?: boolean;
|
||||||
|
_$forwardPropsSet?: Set<HTMLElement | ComposableNode>;
|
||||||
|
_$forwardPropsId?: string[];
|
||||||
|
_$contentKey?: string;
|
||||||
|
_$depNumsToUpdate?: number[];
|
||||||
|
updateState: (bit: number) => void;
|
||||||
|
updateProp: (...args: any[]) => void;
|
||||||
|
updateContext?: (context: any, key: string, value: any) => void;
|
||||||
|
getUpdateViews?: () => any;
|
||||||
|
didUnmount?: () => void;
|
||||||
|
willUnmount?: () => void;
|
||||||
|
didMount?: () => void;
|
||||||
|
updateView?: (depNum: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompNode extends ComposableNode {
|
||||||
|
__type: InulaNodeType.Comp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HookNode extends ComposableNode {
|
||||||
|
__type: InulaNodeType.Hook;
|
||||||
|
bitmap: number;
|
||||||
|
parent: HookNode | CompNode;
|
||||||
|
value?: () => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Mutable node is a node that this._$nodes can be changed, things need to pay attention:
|
||||||
|
* 1. The context of the new nodes should be the same as the old nodes
|
||||||
|
* 2. The new nodes should be added to the parentEl
|
||||||
|
* 3. The old nodes should be removed from the parentEl
|
||||||
|
*/
|
||||||
|
export interface MutableNode<UnmountShape = ScopedLifecycle> extends VNode {
|
||||||
|
willUnmountFuncs: UnmountShape;
|
||||||
|
didUnmountFuncs: UnmountShape;
|
||||||
|
savedContextNodes: Map<symbol, ContextNode<any>> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CondNode extends MutableNode {
|
||||||
|
cond: number;
|
||||||
|
didntChange: boolean;
|
||||||
|
__type: InulaNodeType.Cond;
|
||||||
|
depNum: number;
|
||||||
|
condFunc: (condNode: CondNode) => VNode[];
|
||||||
|
/**
|
||||||
|
* @brief assigned by condNode.condFunc in compile time
|
||||||
|
* @param changed
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
updateFunc?: (changed: number) => void;
|
||||||
|
_$parentEl?: InulaHTMLNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpNode extends MutableNode {
|
||||||
|
__type: InulaNodeType.Exp;
|
||||||
|
_$nodes: InulaNode[];
|
||||||
|
_$parentEl?: InulaHTMLNode;
|
||||||
|
deps: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ForNode<T> extends MutableNode<Map<number, ScopedLifecycle>>, VNode {
|
||||||
|
array: T[];
|
||||||
|
__type: InulaNodeType.For;
|
||||||
|
depNum: number;
|
||||||
|
keys: number[];
|
||||||
|
nodeFunc: (item: T, idx: number, updateArr: any[]) => VNode[];
|
||||||
|
_$nodes: InulaNode[];
|
||||||
|
_$parentEl?: InulaHTMLNode;
|
||||||
|
nodesMap: Map<number, InulaNode[]>;
|
||||||
|
updateArr: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Context<V extends Record<PropertyKey, any> | null> {
|
||||||
|
id: symbol;
|
||||||
|
value: V | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextNode<V extends Record<PropertyKey, any>> {
|
||||||
|
value: V;
|
||||||
|
context: Context<V>;
|
||||||
|
__type: InulaNodeType.Context;
|
||||||
|
depMap: Record<keyof V, Array<unknown>>;
|
||||||
|
_$nodes: VNode[];
|
||||||
|
_$unmounted?: boolean;
|
||||||
|
consumers: Set<CompNode | HookNode>;
|
||||||
|
prevValue?: V | null;
|
||||||
|
prevContextNode?: ContextNode<V> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Updater = (changed: number) => void;
|
||||||
|
|
||||||
|
export interface ChildrenNode {
|
||||||
|
__type: InulaNodeType.Children;
|
||||||
|
childrenFunc: (addUpdate: (updater: Updater) => void) => VNode[];
|
||||||
|
updaters: Set<Updater>;
|
||||||
|
}
|
|
@ -1,74 +0,0 @@
|
||||||
import { type DLightHTMLAttributes } from './htmlTag';
|
|
||||||
|
|
||||||
// a very magical solution
|
|
||||||
// when vscode parse ts, if it is type A<T> = B<xxx<T>>, it will show the detailed type,
|
|
||||||
// but if type A<T> = B<xxx<T>> & xxx, it will only show alias (here is A)
|
|
||||||
// because I don't want to expose the detailed type, so type A<T> = B<xxx<T>> & Useless
|
|
||||||
// but if type Useless = { useless: never } will cause this type to have an additional property userless
|
|
||||||
// so just don't add key!
|
|
||||||
type Useless = { [key in '']: never };
|
|
||||||
|
|
||||||
export type DLightObject<T> = {
|
|
||||||
[K in keyof T]-?: undefined extends T[K]
|
|
||||||
? (value?: T[K]) => DLightObject<Omit<T, K>>
|
|
||||||
: (value: T[K]) => DLightObject<Omit<T, K>>;
|
|
||||||
};
|
|
||||||
interface CustomNodeProps {
|
|
||||||
willMount: (node: any) => void;
|
|
||||||
didMount: (node: any) => void;
|
|
||||||
willUnmount: (node: any) => void;
|
|
||||||
didUnmount: (node: any) => void;
|
|
||||||
didUpdate: (node: any, key: string, prevValue: any, currValue: any) => void;
|
|
||||||
ref: (node: any) => void;
|
|
||||||
elements: HTMLElement[] | ((holder: HTMLElement[]) => void) | undefined;
|
|
||||||
forwardProps: true | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ContentProp<T = object> = T & { _$idContent: true };
|
|
||||||
|
|
||||||
export type RemoveOptional<T> = {
|
|
||||||
[K in keyof T]-?: T[K];
|
|
||||||
};
|
|
||||||
|
|
||||||
type IsAny<T> = { _$isAny: true } extends T ? true : false;
|
|
||||||
|
|
||||||
export type ContentKeyName<T> = {
|
|
||||||
[K in keyof T]: IsAny<T[K]> extends true
|
|
||||||
? never
|
|
||||||
: // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
T[K] extends ContentProp<infer _>
|
|
||||||
? K
|
|
||||||
: never;
|
|
||||||
}[keyof T];
|
|
||||||
|
|
||||||
export type CheckContent<T> = RemoveOptional<T>[ContentKeyName<RemoveOptional<T>>];
|
|
||||||
|
|
||||||
type CustomClassTag<T, O> =
|
|
||||||
ContentKeyName<RemoveOptional<O>> extends undefined
|
|
||||||
? () => DLightObject<T>
|
|
||||||
: undefined extends O[ContentKeyName<RemoveOptional<O>>]
|
|
||||||
? CheckContent<O> extends ContentProp<infer U>
|
|
||||||
? (content?: U extends unknown ? any : unknown) => DLightObject<Omit<T, ContentKeyName<RemoveOptional<O>>>>
|
|
||||||
: never
|
|
||||||
: CheckContent<O> extends ContentProp<infer U>
|
|
||||||
? (content: U extends unknown ? any : unknown) => DLightObject<Omit<T, ContentKeyName<RemoveOptional<O>>>>
|
|
||||||
: never;
|
|
||||||
|
|
||||||
type CustomSnippetTag<T> = T extends { content: infer U }
|
|
||||||
? (content: U) => DLightObject<Omit<T, 'content'>>
|
|
||||||
: T extends { content?: infer U }
|
|
||||||
? (content?: U) => DLightObject<Omit<T, 'content'>>
|
|
||||||
: () => DLightObject<T>;
|
|
||||||
|
|
||||||
type CustomTagType<T, G> = CustomClassTag<
|
|
||||||
T & CustomNodeProps & (keyof G extends never ? object : DLightHTMLAttributes<G, object, HTMLElement>),
|
|
||||||
T
|
|
||||||
> &
|
|
||||||
Useless;
|
|
||||||
export type Typed<T = object, G = object> = CustomTagType<T, G> & Useless;
|
|
||||||
export type SnippetTyped<T = object> = CustomSnippetTag<T> & Useless;
|
|
||||||
|
|
||||||
export type Pretty = any;
|
|
||||||
|
|
||||||
// ---- reverse
|
|
||||||
export type UnTyped<T> = T extends Typed<infer U> ? U : never;
|
|
|
@ -1,6 +0,0 @@
|
||||||
// ---- env
|
|
||||||
import { DLightObject } from './compTag';
|
|
||||||
|
|
||||||
type AnyEnv = { _$anyEnv: true };
|
|
||||||
|
|
||||||
export const env: <T = AnyEnv>() => T extends AnyEnv ? any : DLightObject<T>;
|
|
|
@ -1,13 +0,0 @@
|
||||||
interface ExpressionTag {
|
|
||||||
willMount: (node: any) => void;
|
|
||||||
didMount: (node: any) => void;
|
|
||||||
willUnmount: (node: any) => void;
|
|
||||||
didUnmount: (node: any) => void;
|
|
||||||
didUpdate: <T>(node: any, key: string, prevValue: T, currValue: T) => void;
|
|
||||||
elements: HTMLElement[] | ((holder: HTMLElement[]) => void) | undefined;
|
|
||||||
ref: (node: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExpressionTagFunc = (nodes: any) => ExpressionTag;
|
|
||||||
|
|
||||||
export const _: ExpressionTagFunc;
|
|
|
@ -1,516 +0,0 @@
|
||||||
export interface DLightGlobalEventHandlers {
|
|
||||||
/**
|
|
||||||
* Fires when the user aborts the download.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/abort_event)
|
|
||||||
*/
|
|
||||||
onAbort: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationcancel_event) */
|
|
||||||
onAnimationCancel: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationend_event) */
|
|
||||||
onAnimationEnd: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationiteration_event) */
|
|
||||||
onAnimationIteration: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationstart_event) */
|
|
||||||
onAnimationStart: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/auxclick_event) */
|
|
||||||
onAuxClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/beforeinput_event) */
|
|
||||||
onBeforeInput: ((this: GlobalEventHandlers, ev: InputEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the object loses the input focus.
|
|
||||||
* @param ev The focus event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/blur_event)
|
|
||||||
*/
|
|
||||||
onBlur: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/cancel_event) */
|
|
||||||
onCancel: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when playback is possible, but would require further buffering.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/canplay_event)
|
|
||||||
*/
|
|
||||||
onCanPlay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/canplaythrough_event) */
|
|
||||||
onCanPlayThrough: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the contents of the object or selection have changed.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)
|
|
||||||
*/
|
|
||||||
onChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user clicks the left mouse button on the object
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/click_event)
|
|
||||||
*/
|
|
||||||
onClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/close_event) */
|
|
||||||
onClose: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user clicks the right mouse button in the client area, opening the context menu.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/contextmenu_event)
|
|
||||||
*/
|
|
||||||
onContextMenu: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/copy_event) */
|
|
||||||
onCopy: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLTrackElement/cuechange_event) */
|
|
||||||
onCueChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/cut_event) */
|
|
||||||
onCut: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user double-clicks the object.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/dblclick_event)
|
|
||||||
*/
|
|
||||||
onDblClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires on the source object continuously during a drag operation.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/drag_event)
|
|
||||||
*/
|
|
||||||
onDrag: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires on the source object when the user releases the mouse at the close of a drag operation.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragend_event)
|
|
||||||
*/
|
|
||||||
onDragEnd: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires on the target element when the user drags the object to a valid drop target.
|
|
||||||
* @param ev The drag event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragenter_event)
|
|
||||||
*/
|
|
||||||
onDragEnter: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires on the target object when the user moves the mouse out of a valid drop target during a drag operation.
|
|
||||||
* @param ev The drag event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragleave_event)
|
|
||||||
*/
|
|
||||||
onDragLeave: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires on the target element continuously while the user drags the object over a valid drop target.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragover_event)
|
|
||||||
*/
|
|
||||||
onDragOver: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires on the source object when the user starts to drag a text selection or selected object.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragstart_event)
|
|
||||||
*/
|
|
||||||
onDragStart: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/drop_event) */
|
|
||||||
onDrop: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the duration attribute is updated.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/durationchange_event)
|
|
||||||
*/
|
|
||||||
onDurationChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the media element is reset to its initial state.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/emptied_event)
|
|
||||||
*/
|
|
||||||
onEmptied: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the end of playback is reached.
|
|
||||||
* @param ev The event
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/ended_event)
|
|
||||||
*/
|
|
||||||
onEnded: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when an error occurs during object loading.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/error_event)
|
|
||||||
*/
|
|
||||||
onError: OnErrorEventHandler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the object receives focus.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/focus_event)
|
|
||||||
*/
|
|
||||||
onFocus: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/formdata_event) */
|
|
||||||
onFormData: ((this: GlobalEventHandlers, ev: FormDataEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/gotpointercapture_event) */
|
|
||||||
onGotPointerCapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event) */
|
|
||||||
onInput: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/invalid_event) */
|
|
||||||
onInvalid: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user presses a key.
|
|
||||||
* @param ev The keyboard event
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keydown_event)
|
|
||||||
*/
|
|
||||||
onKeyDown: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user presses an alphanumeric key.
|
|
||||||
* @param ev The event.
|
|
||||||
* @deprecated
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keypress_event)
|
|
||||||
*/
|
|
||||||
onKeyPress: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user releases a key.
|
|
||||||
* @param ev The keyboard event
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keyup_event)
|
|
||||||
*/
|
|
||||||
onKeyUp: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires immediately after the browser loads the object.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SVGElement/load_event)
|
|
||||||
*/
|
|
||||||
onLoad: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when media data is loaded at the current playback position.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadeddata_event)
|
|
||||||
*/
|
|
||||||
onLoadedData: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the duration and dimensions of the media have been determined.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadedmetadata_event)
|
|
||||||
*/
|
|
||||||
onLoadedMetadata: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when Internet Explorer begins looking for media data.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadstart_event)
|
|
||||||
*/
|
|
||||||
onLoadStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/lostpointercapture_event) */
|
|
||||||
onLostPointerCapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user clicks the object with either mouse button.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mousedown_event)
|
|
||||||
*/
|
|
||||||
onMouseDown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseenter_event) */
|
|
||||||
onMouseEnter: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseleave_event) */
|
|
||||||
onMouseLeave: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user moves the mouse over the object.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mousemove_event)
|
|
||||||
*/
|
|
||||||
onMouseMove: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user moves the mouse pointer outside the boundaries of the object.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseout_event)
|
|
||||||
*/
|
|
||||||
onMouseOut: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user moves the mouse pointer into the object.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseover_event)
|
|
||||||
*/
|
|
||||||
onMouseOver: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user releases a mouse button while the mouse is over the object.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseup_event)
|
|
||||||
*/
|
|
||||||
onMouseUp: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/paste_event) */
|
|
||||||
onPaste: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when playback is paused.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/pause_event)
|
|
||||||
*/
|
|
||||||
onPause: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the play method is requested.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/play_event)
|
|
||||||
*/
|
|
||||||
onPlay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the audio or video has started playing.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/playing_event)
|
|
||||||
*/
|
|
||||||
onPlaying: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointercancel_event) */
|
|
||||||
onPointerCancel: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerdown_event) */
|
|
||||||
onPointerDown: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerenter_event) */
|
|
||||||
onPointerEnter: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerleave_event) */
|
|
||||||
onPointerLeave: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointermove_event) */
|
|
||||||
onPointerMove: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerout_event) */
|
|
||||||
onPointerOut: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerover_event) */
|
|
||||||
onPointerOver: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerup_event) */
|
|
||||||
onPointerUp: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs to indicate progress while downloading media data.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/progress_event)
|
|
||||||
*/
|
|
||||||
onProgress: ((this: GlobalEventHandlers, ev: ProgressEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the playback rate is increased or decreased.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/ratechange_event)
|
|
||||||
*/
|
|
||||||
onRateChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user resets a form.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/reset_event)
|
|
||||||
*/
|
|
||||||
onReset: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLVideoElement/resize_event) */
|
|
||||||
onResize: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user repositions the scroll box in the scroll bar on the object.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/scroll_event)
|
|
||||||
*/
|
|
||||||
onScroll: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/scrollend_event) */
|
|
||||||
onScrollEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/securitypolicyviolation_event) */
|
|
||||||
onSecurityPolicyViolation: ((this: GlobalEventHandlers, ev: SecurityPolicyViolationEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the seek operation ends.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/seeked_event)
|
|
||||||
*/
|
|
||||||
onSeeked: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the current playback position is moved.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/seeking_event)
|
|
||||||
*/
|
|
||||||
onSeeking: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the current selection changes.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/select_event)
|
|
||||||
*/
|
|
||||||
onSelect: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/selectionchange_event) */
|
|
||||||
onSelectionChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/selectstart_event) */
|
|
||||||
onSelectStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLSlotElement/slotchange_event) */
|
|
||||||
onSlotChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the download has stopped.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/stalled_event)
|
|
||||||
*/
|
|
||||||
onStalled: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/submit_event) */
|
|
||||||
onSubmit: ((this: GlobalEventHandlers, ev: SubmitEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs if the load operation has been intentionally halted.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/suspend_event)
|
|
||||||
*/
|
|
||||||
onSuspend: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs to indicate the current playback position.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/timeupdate_event)
|
|
||||||
*/
|
|
||||||
onTimeUpdate: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDetailsElement/toggle_event) */
|
|
||||||
onToggle: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitioncancel_event) */
|
|
||||||
onTransitionCancel: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionend_event) */
|
|
||||||
onTransitionEnd: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionrun_event) */
|
|
||||||
onTransitionRun: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionstart_event) */
|
|
||||||
onTransitionStart: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the volume is changed, or playback is muted or unmuted.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/volumechange_event)
|
|
||||||
*/
|
|
||||||
onVolumeChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when playback stops because the next frame of a video resource is not available.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/waiting_event)
|
|
||||||
*/
|
|
||||||
onWaiting: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This is a legacy alias of `onAnimationEnd`.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationend_event)
|
|
||||||
*/
|
|
||||||
onWebkitAnimationEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This is a legacy alias of `onAnimationIteration`.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationiteration_event)
|
|
||||||
*/
|
|
||||||
onWebkitAnimationIteration: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This is a legacy alias of `onAnimationStart`.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationstart_event)
|
|
||||||
*/
|
|
||||||
onWebkitAnimationStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This is a legacy alias of `onTransitionEnd`.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionend_event)
|
|
||||||
*/
|
|
||||||
onWebkitTransitionEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/wheel_event) */
|
|
||||||
onWheel: ((this: GlobalEventHandlers, ev: WheelEvent) => any) | null;
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { type Properties } from 'csstype';
|
|
||||||
|
|
||||||
// ---- Used to determine whether X and Y are equal, return A if equal, otherwise B
|
|
||||||
type IfEquals<X, Y, A, B> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B;
|
|
||||||
|
|
||||||
export type OmitIndexSignature<ObjectType> = {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
[KeyType in keyof ObjectType as {} extends Record<KeyType, unknown> ? never : KeyType]: ObjectType[KeyType];
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---- For each key, check whether there is readonly, if there is, return never, and then Pick out is not never
|
|
||||||
type WritableKeysOf<T> = {
|
|
||||||
[P in keyof T]: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P, never>;
|
|
||||||
}[keyof T];
|
|
||||||
type RemoveReadOnly<T> = Pick<T, WritableKeysOf<T>>;
|
|
||||||
|
|
||||||
// ---- Delete all functions
|
|
||||||
type OmitFunction<T> = Omit<T, { [K in keyof T]: T[K] extends (...args: any) => any ? K : never }[keyof T]>;
|
|
||||||
|
|
||||||
type OmitFuncAndReadOnly<T> = RemoveReadOnly<OmitFunction<OmitIndexSignature<T>>>;
|
|
||||||
|
|
||||||
// ---- properties
|
|
||||||
type OmitFuncAndReadOnlyProperty<G> = Omit<OmitFuncAndReadOnly<G>, 'className' | 'htmlFor' | 'style' | 'innerText'>;
|
|
||||||
|
|
||||||
type CustomCSSProperties = {
|
|
||||||
[Key in `--${string}`]: string | number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type HTMLAttributes<T> = OmitFuncAndReadOnlyProperty<T> & {
|
|
||||||
style: Properties & CustomCSSProperties;
|
|
||||||
class: string;
|
|
||||||
for: string;
|
|
||||||
};
|
|
|
@ -1,236 +0,0 @@
|
||||||
import type { DLightGlobalEventHandlers } from './event';
|
|
||||||
import type { OmitIndexSignature, HTMLAttributes } from './htmlElement';
|
|
||||||
|
|
||||||
// ---- If there is an event(start with on), remove it
|
|
||||||
export type PropertyWithEvent<G> = Omit<
|
|
||||||
G,
|
|
||||||
{
|
|
||||||
[K in keyof G]: K extends `on${string}` ? K : never;
|
|
||||||
}[keyof G]
|
|
||||||
> &
|
|
||||||
DLightGlobalEventHandlers;
|
|
||||||
|
|
||||||
interface DLightHtmlProps<El> {
|
|
||||||
ref: El | ((holder: El) => void) | undefined;
|
|
||||||
prop: Record<string, string | number | boolean>;
|
|
||||||
attr: Record<string, string>;
|
|
||||||
dataset: Record<string, string>;
|
|
||||||
forwardProps: true | undefined;
|
|
||||||
willMount: (el: El) => void;
|
|
||||||
didMount: (el: El) => void;
|
|
||||||
willUnmount: (el: El) => void;
|
|
||||||
didUnmount: (el: El) => void;
|
|
||||||
didUpdate: <T>(el: El, key: string, prevValue: T, currValue: T) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DLightHTMLAttributes<T, G, El> = DLightHtmlProps<El> & HTMLAttributes<T> & G;
|
|
||||||
|
|
||||||
export type DLightHTMLAttributesFunc<T, G, El> = {
|
|
||||||
[K in keyof DLightHTMLAttributes<T, G, El>]: (
|
|
||||||
value?: DLightHTMLAttributes<T, G, El>[K]
|
|
||||||
) => Omit<DLightHTMLAttributesFunc<T, G, El>, K>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DLightHtmlTagFunc<T = HTMLElement, G = object> = (
|
|
||||||
innerText?: string | number | ((View: never) => void)
|
|
||||||
) => DLightHTMLAttributesFunc<PropertyWithEvent<OmitIndexSignature<T>>, G, T>;
|
|
||||||
|
|
||||||
export const a: DLightHtmlTagFunc<HTMLAnchorElement>;
|
|
||||||
export const abbr: DLightHtmlTagFunc;
|
|
||||||
export const address: DLightHtmlTagFunc;
|
|
||||||
export const area: DLightHtmlTagFunc<HTMLAreaElement>;
|
|
||||||
export const article: DLightHtmlTagFunc;
|
|
||||||
export const aside: DLightHtmlTagFunc;
|
|
||||||
export const audio: DLightHtmlTagFunc<HTMLAudioElement>;
|
|
||||||
export const b: DLightHtmlTagFunc;
|
|
||||||
export const base: DLightHtmlTagFunc<HTMLBaseElement>;
|
|
||||||
export const bdi: DLightHtmlTagFunc;
|
|
||||||
export const bdo: DLightHtmlTagFunc;
|
|
||||||
export const blockquote: DLightHtmlTagFunc<HTMLQuoteElement>;
|
|
||||||
export const body: DLightHtmlTagFunc<HTMLBodyElement>;
|
|
||||||
export const br: DLightHtmlTagFunc<HTMLBRElement>;
|
|
||||||
export const button: DLightHtmlTagFunc<HTMLButtonElement>;
|
|
||||||
export const canvas: DLightHtmlTagFunc<HTMLCanvasElement>;
|
|
||||||
export const caption: DLightHtmlTagFunc<HTMLTableCaptionElement>;
|
|
||||||
export const cite: DLightHtmlTagFunc;
|
|
||||||
export const code: DLightHtmlTagFunc;
|
|
||||||
export const col: DLightHtmlTagFunc<HTMLTableColElement>;
|
|
||||||
export const colgroup: DLightHtmlTagFunc<HTMLTableColElement>;
|
|
||||||
export const data: DLightHtmlTagFunc<HTMLDataElement>;
|
|
||||||
export const datalist: DLightHtmlTagFunc<HTMLDataListElement>;
|
|
||||||
export const dd: DLightHtmlTagFunc;
|
|
||||||
export const del: DLightHtmlTagFunc<HTMLModElement>;
|
|
||||||
export const details: DLightHtmlTagFunc<HTMLDetailsElement>;
|
|
||||||
export const dfn: DLightHtmlTagFunc;
|
|
||||||
export const dialog: DLightHtmlTagFunc<HTMLDialogElement>;
|
|
||||||
export const div: DLightHtmlTagFunc<HTMLDivElement>;
|
|
||||||
export const dl: DLightHtmlTagFunc<HTMLDListElement>;
|
|
||||||
export const dt: DLightHtmlTagFunc;
|
|
||||||
export const em: DLightHtmlTagFunc;
|
|
||||||
export const embed: DLightHtmlTagFunc<HTMLEmbedElement>;
|
|
||||||
export const fieldset: DLightHtmlTagFunc<HTMLFieldSetElement>;
|
|
||||||
export const figcaption: DLightHtmlTagFunc;
|
|
||||||
export const figure: DLightHtmlTagFunc;
|
|
||||||
export const footer: DLightHtmlTagFunc;
|
|
||||||
export const form: DLightHtmlTagFunc<HTMLFormElement>;
|
|
||||||
export const h1: DLightHtmlTagFunc<HTMLHeadingElement>;
|
|
||||||
export const h2: DLightHtmlTagFunc<HTMLHeadingElement>;
|
|
||||||
export const h3: DLightHtmlTagFunc<HTMLHeadingElement>;
|
|
||||||
export const h4: DLightHtmlTagFunc<HTMLHeadingElement>;
|
|
||||||
export const h5: DLightHtmlTagFunc<HTMLHeadingElement>;
|
|
||||||
export const h6: DLightHtmlTagFunc<HTMLHeadingElement>;
|
|
||||||
export const head: DLightHtmlTagFunc<HTMLHeadElement>;
|
|
||||||
export const header: DLightHtmlTagFunc;
|
|
||||||
export const hgroup: DLightHtmlTagFunc;
|
|
||||||
export const hr: DLightHtmlTagFunc<HTMLHRElement>;
|
|
||||||
export const html: DLightHtmlTagFunc<HTMLHtmlElement>;
|
|
||||||
export const i: DLightHtmlTagFunc;
|
|
||||||
export const iframe: DLightHtmlTagFunc<HTMLIFrameElement>;
|
|
||||||
export const img: DLightHtmlTagFunc<HTMLImageElement>;
|
|
||||||
export const input: DLightHtmlTagFunc<HTMLInputElement>;
|
|
||||||
export const ins: DLightHtmlTagFunc<HTMLModElement>;
|
|
||||||
export const kbd: DLightHtmlTagFunc;
|
|
||||||
export const label: DLightHtmlTagFunc<HTMLLabelElement>;
|
|
||||||
export const legend: DLightHtmlTagFunc<HTMLLegendElement>;
|
|
||||||
export const li: DLightHtmlTagFunc<HTMLLIElement>;
|
|
||||||
export const link: DLightHtmlTagFunc<HTMLLinkElement>;
|
|
||||||
export const main: DLightHtmlTagFunc;
|
|
||||||
export const map: DLightHtmlTagFunc<HTMLMapElement>;
|
|
||||||
export const mark: DLightHtmlTagFunc;
|
|
||||||
export const menu: DLightHtmlTagFunc<HTMLMenuElement>;
|
|
||||||
export const meta: DLightHtmlTagFunc<HTMLMetaElement>;
|
|
||||||
export const meter: DLightHtmlTagFunc<HTMLMeterElement>;
|
|
||||||
export const nav: DLightHtmlTagFunc;
|
|
||||||
export const noscript: DLightHtmlTagFunc;
|
|
||||||
export const object: DLightHtmlTagFunc<HTMLObjectElement>;
|
|
||||||
export const ol: DLightHtmlTagFunc<HTMLOListElement>;
|
|
||||||
export const optgroup: DLightHtmlTagFunc<HTMLOptGroupElement>;
|
|
||||||
export const option: DLightHtmlTagFunc<HTMLOptionElement>;
|
|
||||||
export const output: DLightHtmlTagFunc<HTMLOutputElement>;
|
|
||||||
export const p: DLightHtmlTagFunc<HTMLParagraphElement>;
|
|
||||||
export const picture: DLightHtmlTagFunc<HTMLPictureElement>;
|
|
||||||
export const pre: DLightHtmlTagFunc<HTMLPreElement>;
|
|
||||||
export const progress: DLightHtmlTagFunc<HTMLProgressElement>;
|
|
||||||
export const q: DLightHtmlTagFunc<HTMLQuoteElement>;
|
|
||||||
export const rp: DLightHtmlTagFunc;
|
|
||||||
export const rt: DLightHtmlTagFunc;
|
|
||||||
export const ruby: DLightHtmlTagFunc;
|
|
||||||
export const s: DLightHtmlTagFunc;
|
|
||||||
export const samp: DLightHtmlTagFunc;
|
|
||||||
export const script: DLightHtmlTagFunc<HTMLScriptElement>;
|
|
||||||
export const section: DLightHtmlTagFunc;
|
|
||||||
export const select: DLightHtmlTagFunc<HTMLSelectElement>;
|
|
||||||
export const slot: DLightHtmlTagFunc<HTMLSlotElement>;
|
|
||||||
export const small: DLightHtmlTagFunc;
|
|
||||||
export const source: DLightHtmlTagFunc<HTMLSourceElement>;
|
|
||||||
export const span: DLightHtmlTagFunc<HTMLSpanElement>;
|
|
||||||
export const strong: DLightHtmlTagFunc;
|
|
||||||
export const style: DLightHtmlTagFunc<HTMLStyleElement>;
|
|
||||||
export const sub: DLightHtmlTagFunc;
|
|
||||||
export const summary: DLightHtmlTagFunc;
|
|
||||||
export const sup: DLightHtmlTagFunc;
|
|
||||||
export const table: DLightHtmlTagFunc<HTMLTableElement>;
|
|
||||||
export const tbody: DLightHtmlTagFunc<HTMLTableSectionElement>;
|
|
||||||
export const td: DLightHtmlTagFunc<HTMLTableCellElement>;
|
|
||||||
export const template: DLightHtmlTagFunc<HTMLTemplateElement>;
|
|
||||||
export const textarea: DLightHtmlTagFunc<HTMLTextAreaElement>;
|
|
||||||
export const tfoot: DLightHtmlTagFunc<HTMLTableSectionElement>;
|
|
||||||
export const th: DLightHtmlTagFunc<HTMLTableCellElement>;
|
|
||||||
export const thead: DLightHtmlTagFunc<HTMLTableSectionElement>;
|
|
||||||
export const time: DLightHtmlTagFunc<HTMLTimeElement>;
|
|
||||||
export const title: DLightHtmlTagFunc<HTMLTitleElement>;
|
|
||||||
export const tr: DLightHtmlTagFunc<HTMLTableRowElement>;
|
|
||||||
export const track: DLightHtmlTagFunc<HTMLTrackElement>;
|
|
||||||
export const u: DLightHtmlTagFunc;
|
|
||||||
export const ul: DLightHtmlTagFunc<HTMLUListElement>;
|
|
||||||
export const var_: DLightHtmlTagFunc;
|
|
||||||
export const video: DLightHtmlTagFunc<HTMLVideoElement>;
|
|
||||||
export const wbr: DLightHtmlTagFunc;
|
|
||||||
export const acronym: DLightHtmlTagFunc;
|
|
||||||
export const applet: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const basefont: DLightHtmlTagFunc;
|
|
||||||
export const bgsound: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const big: DLightHtmlTagFunc;
|
|
||||||
export const blink: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const center: DLightHtmlTagFunc;
|
|
||||||
export const dir: DLightHtmlTagFunc<HTMLDirectoryElement>;
|
|
||||||
export const font: DLightHtmlTagFunc<HTMLFontElement>;
|
|
||||||
export const frame: DLightHtmlTagFunc<HTMLFrameElement>;
|
|
||||||
export const frameset: DLightHtmlTagFunc<HTMLFrameSetElement>;
|
|
||||||
export const isindex: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const keygen: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const listing: DLightHtmlTagFunc<HTMLPreElement>;
|
|
||||||
export const marquee: DLightHtmlTagFunc<HTMLMarqueeElement>;
|
|
||||||
export const menuitem: DLightHtmlTagFunc;
|
|
||||||
export const multicol: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const nextid: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const nobr: DLightHtmlTagFunc;
|
|
||||||
export const noembed: DLightHtmlTagFunc;
|
|
||||||
export const noframes: DLightHtmlTagFunc;
|
|
||||||
export const param: DLightHtmlTagFunc<HTMLParamElement>;
|
|
||||||
export const plaintext: DLightHtmlTagFunc;
|
|
||||||
export const rb: DLightHtmlTagFunc;
|
|
||||||
export const rtc: DLightHtmlTagFunc;
|
|
||||||
export const spacer: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const strike: DLightHtmlTagFunc;
|
|
||||||
export const tt: DLightHtmlTagFunc;
|
|
||||||
export const xmp: DLightHtmlTagFunc<HTMLPreElement>;
|
|
||||||
export const animate: DLightHtmlTagFunc<SVGAnimateElement>;
|
|
||||||
export const animateMotion: DLightHtmlTagFunc<SVGAnimateMotionElement>;
|
|
||||||
export const animateTransform: DLightHtmlTagFunc<SVGAnimateTransformElement>;
|
|
||||||
export const circle: DLightHtmlTagFunc<SVGCircleElement>;
|
|
||||||
export const clipPath: DLightHtmlTagFunc<SVGClipPathElement>;
|
|
||||||
export const defs: DLightHtmlTagFunc<SVGDefsElement>;
|
|
||||||
export const desc: DLightHtmlTagFunc<SVGDescElement>;
|
|
||||||
export const ellipse: DLightHtmlTagFunc<SVGEllipseElement>;
|
|
||||||
export const feBlend: DLightHtmlTagFunc<SVGFEBlendElement>;
|
|
||||||
export const feColorMatrix: DLightHtmlTagFunc<SVGFEColorMatrixElement>;
|
|
||||||
export const feComponentTransfer: DLightHtmlTagFunc<SVGFEComponentTransferElement>;
|
|
||||||
export const feComposite: DLightHtmlTagFunc<SVGFECompositeElement>;
|
|
||||||
export const feConvolveMatrix: DLightHtmlTagFunc<SVGFEConvolveMatrixElement>;
|
|
||||||
export const feDiffuseLighting: DLightHtmlTagFunc<SVGFEDiffuseLightingElement>;
|
|
||||||
export const feDisplacementMap: DLightHtmlTagFunc<SVGFEDisplacementMapElement>;
|
|
||||||
export const feDistantLight: DLightHtmlTagFunc<SVGFEDistantLightElement>;
|
|
||||||
export const feDropShadow: DLightHtmlTagFunc<SVGFEDropShadowElement>;
|
|
||||||
export const feFlood: DLightHtmlTagFunc<SVGFEFloodElement>;
|
|
||||||
export const feFuncA: DLightHtmlTagFunc<SVGFEFuncAElement>;
|
|
||||||
export const feFuncB: DLightHtmlTagFunc<SVGFEFuncBElement>;
|
|
||||||
export const feFuncG: DLightHtmlTagFunc<SVGFEFuncGElement>;
|
|
||||||
export const feFuncR: DLightHtmlTagFunc<SVGFEFuncRElement>;
|
|
||||||
export const feGaussianBlur: DLightHtmlTagFunc<SVGFEGaussianBlurElement>;
|
|
||||||
export const feImage: DLightHtmlTagFunc<SVGFEImageElement>;
|
|
||||||
export const feMerge: DLightHtmlTagFunc<SVGFEMergeElement>;
|
|
||||||
export const feMergeNode: DLightHtmlTagFunc<SVGFEMergeNodeElement>;
|
|
||||||
export const feMorphology: DLightHtmlTagFunc<SVGFEMorphologyElement>;
|
|
||||||
export const feOffset: DLightHtmlTagFunc<SVGFEOffsetElement>;
|
|
||||||
export const fePointLight: DLightHtmlTagFunc<SVGFEPointLightElement>;
|
|
||||||
export const feSpecularLighting: DLightHtmlTagFunc<SVGFESpecularLightingElement>;
|
|
||||||
export const feSpotLight: DLightHtmlTagFunc<SVGFESpotLightElement>;
|
|
||||||
export const feTile: DLightHtmlTagFunc<SVGFETileElement>;
|
|
||||||
export const feTurbulence: DLightHtmlTagFunc<SVGFETurbulenceElement>;
|
|
||||||
export const filter: DLightHtmlTagFunc<SVGFilterElement>;
|
|
||||||
export const foreignObject: DLightHtmlTagFunc<SVGForeignObjectElement>;
|
|
||||||
export const g: DLightHtmlTagFunc<SVGGElement>;
|
|
||||||
export const image: DLightHtmlTagFunc<SVGImageElement>;
|
|
||||||
export const line: DLightHtmlTagFunc<SVGLineElement>;
|
|
||||||
export const linearGradient: DLightHtmlTagFunc<SVGLinearGradientElement>;
|
|
||||||
export const marker: DLightHtmlTagFunc<SVGMarkerElement>;
|
|
||||||
export const mask: DLightHtmlTagFunc<SVGMaskElement>;
|
|
||||||
export const metadata: DLightHtmlTagFunc<SVGMetadataElement>;
|
|
||||||
export const mpath: DLightHtmlTagFunc<SVGMPathElement>;
|
|
||||||
export const path: DLightHtmlTagFunc<SVGPathElement>;
|
|
||||||
export const pattern: DLightHtmlTagFunc<SVGPatternElement>;
|
|
||||||
export const polygon: DLightHtmlTagFunc<SVGPolygonElement>;
|
|
||||||
export const polyline: DLightHtmlTagFunc<SVGPolylineElement>;
|
|
||||||
export const radialGradient: DLightHtmlTagFunc<SVGRadialGradientElement>;
|
|
||||||
export const rect: DLightHtmlTagFunc<SVGRectElement>;
|
|
||||||
export const set: DLightHtmlTagFunc<SVGSetElement>;
|
|
||||||
export const stop: DLightHtmlTagFunc<SVGStopElement>;
|
|
||||||
export const svg: DLightHtmlTagFunc<SVGSVGElement>;
|
|
||||||
export const switch_: DLightHtmlTagFunc<SVGSwitchElement>;
|
|
||||||
export const symbol: DLightHtmlTagFunc<SVGSymbolElement>;
|
|
||||||
export const text: DLightHtmlTagFunc<SVGTextElement>;
|
|
||||||
export const textPath: DLightHtmlTagFunc<SVGTextPathElement>;
|
|
||||||
export const tspan: DLightHtmlTagFunc<SVGTSpanElement>;
|
|
||||||
// export const use: DLightHtmlTagFunc<SVGUseElement>
|
|
||||||
export const view: DLightHtmlTagFunc<SVGViewElement>;
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { type Typed } from './compTag';
|
|
||||||
import { type DLightHtmlTagFunc } from './htmlTag';
|
|
||||||
export { type Properties as CSSProperties } from 'csstype';
|
|
||||||
|
|
||||||
export const comp: <T>(tag: T) => object extends T ? any : Typed<T>;
|
|
||||||
export const tag: (tag: any) => DLightHtmlTagFunc;
|
|
||||||
|
|
||||||
export { _ } from './expressionTag';
|
|
||||||
export * from './htmlTag';
|
|
||||||
export * from './compTag';
|
|
||||||
export * from './envTag';
|
|
||||||
export * from './model';
|
|
||||||
export const Static: any;
|
|
||||||
export const Children: any;
|
|
||||||
export const Content: any;
|
|
||||||
export const Prop: any;
|
|
||||||
export const Env: any;
|
|
||||||
export const Watch: any;
|
|
||||||
export const ForwardProps: any;
|
|
||||||
export const Main: any;
|
|
||||||
export const App: any;
|
|
||||||
export const Mount: (idOrEl: string | HTMLElement) => any;
|
|
||||||
|
|
||||||
// ---- With actual value
|
|
||||||
export function render(DL: any, idOrEl: string | HTMLElement): void;
|
|
||||||
export function manual<T>(callback: () => T, _deps?: any[]): T;
|
|
||||||
export function escape<T>(arg: T): T;
|
|
||||||
export function setGlobal(globalObj: any): void;
|
|
||||||
export function setDocument(customDocument: any): void;
|
|
||||||
export const $: typeof escape;
|
|
||||||
export const View: any;
|
|
||||||
export const Snippet: any;
|
|
||||||
export const Model: any;
|
|
||||||
export const update: any;
|
|
||||||
export const required: any;
|
|
||||||
export function insertChildren<T>(parent: T, children: DLightViewProp): void;
|
|
||||||
|
|
||||||
// ---- View types
|
|
||||||
export type DLightViewComp<T = any> = Typed<T>;
|
|
||||||
export type DLightViewProp = (View: any) => void;
|
|
||||||
export type DLightViewLazy<T = any> = () => Promise<{ default: T }>;
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { ContentKeyName, ContentProp } from './compTag';
|
|
||||||
|
|
||||||
type RemoveDLightInternal<T, Props> = Omit<T, 'willMount' | 'didMount' | 'didUpdate' | 'willUnmount' | keyof Props>;
|
|
||||||
|
|
||||||
export type Modeling<Model, Props = object> = (props: Props) => Model;
|
|
||||||
|
|
||||||
type GetProps<T> = keyof T extends never ? never : ContentKeyName<T> extends undefined ? T : Omit<T, ContentKeyName<T>>;
|
|
||||||
|
|
||||||
type GetContent<T> =
|
|
||||||
ContentKeyName<T> extends undefined ? never : T[ContentKeyName<T>] extends ContentProp<infer U> ? U : never;
|
|
||||||
|
|
||||||
export const use: <M>(
|
|
||||||
model: M,
|
|
||||||
// @ts-expect-error Model should be a function
|
|
||||||
props?: GetProps<Parameters<M>[0]>,
|
|
||||||
// @ts-expect-error Model should be a function
|
|
||||||
content?: GetContent<Parameters<M>[0]>
|
|
||||||
// @ts-expect-error Model should be a function
|
|
||||||
) => RemoveDLightInternal<ReturnType<M>, Parameters<M>[0]>;
|
|
||||||
|
|
||||||
// @ts-expect-error Model should be a function
|
|
||||||
export type ModelType<T> = RemoveDLightInternal<ReturnType<T>, Parameters<T>[0]>;
|
|
|
@ -1,80 +1,247 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
*
|
*
|
||||||
* openInula is licensed under Mulan PSL v2.
|
* openInula is licensed under Mulan PSL v2.
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
*
|
*
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
*
|
*
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, vi } from 'vitest';
|
import { describe, expect, vi } from 'vitest';
|
||||||
import { domTest as it } from './utils';
|
import { domTest as it } from './utils';
|
||||||
import { render, View } from '../src';
|
import { render } from '../src';
|
||||||
|
|
||||||
describe('components', () => {
|
vi.mock('../src/scheduler', async () => {
|
||||||
describe('ref', () => {
|
return {
|
||||||
it('should support ref', ({ container }) => {
|
schedule: (task: () => void) => {
|
||||||
let ref: HTMLElement;
|
task();
|
||||||
|
},
|
||||||
function App() {
|
};
|
||||||
let count = 0;
|
});
|
||||||
let _ref: HTMLElement;
|
|
||||||
|
describe('components', () => {
|
||||||
didMount: {
|
describe('ref', () => {
|
||||||
ref = _ref;
|
it('should support ref', ({ container }) => {
|
||||||
}
|
let ref: HTMLElement;
|
||||||
|
|
||||||
return <div ref={_ref}>test</div>;
|
function App() {
|
||||||
}
|
let count = 0;
|
||||||
|
let _ref: HTMLElement;
|
||||||
render(App, container);
|
|
||||||
|
didMount(() => {
|
||||||
expect(ref).toBeInstanceOf(HTMLElement);
|
ref = _ref;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support ref with function', ({ container }) => {
|
return <div ref={_ref}>test</div>;
|
||||||
const fn = vi.fn();
|
}
|
||||||
|
|
||||||
function App() {
|
render(App, container);
|
||||||
const ref = (el: HTMLElement) => {
|
|
||||||
fn();
|
expect(ref).toBeInstanceOf(HTMLElement);
|
||||||
expect(el).toBeInstanceOf(HTMLElement);
|
});
|
||||||
};
|
|
||||||
|
it('should support ref forwarding', ({ container }) => {
|
||||||
return <div ref={ref}>test</div>;
|
let ref: HTMLElement;
|
||||||
}
|
|
||||||
|
function App() {
|
||||||
render(App, container);
|
let count = 0;
|
||||||
expect(fn).toHaveBeenCalled();
|
let _ref: HTMLElement;
|
||||||
});
|
|
||||||
});
|
didMount(() => {
|
||||||
|
ref = _ref;
|
||||||
describe('env', () => {
|
});
|
||||||
it('should support env', ({ container }) => {
|
|
||||||
function App() {
|
return <Input ref={_ref}>test</Input>;
|
||||||
return (
|
}
|
||||||
<env theme="dark">
|
|
||||||
<Child name="child" />
|
function Input({ ref }) {
|
||||||
</env>
|
return <input ref={ref} />;
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
render(App, container);
|
||||||
function Child({ name }, { theme }) {
|
expect(ref).toBeInstanceOf(HTMLInputElement);
|
||||||
return (
|
});
|
||||||
<div>
|
|
||||||
name is {name}, theme is {theme}
|
it('should support ref with function', ({ container }) => {
|
||||||
</div>
|
const fn = vi.fn();
|
||||||
);
|
|
||||||
}
|
function App() {
|
||||||
|
const ref = (el: HTMLElement) => {
|
||||||
render(App, container);
|
fn();
|
||||||
expect(container.innerHTML).toBe('<div>name is child, theme is dark</div>');
|
expect(el).toBeInstanceOf(HTMLElement);
|
||||||
});
|
};
|
||||||
});
|
|
||||||
});
|
return <div ref={ref}>test</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support comp ref exposing', ({ container }) => {
|
||||||
|
let ref: HTMLElement;
|
||||||
|
const fn = vi.fn();
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let count = 0;
|
||||||
|
let _ref;
|
||||||
|
didMount(() => {
|
||||||
|
ref = _ref;
|
||||||
|
_ref.fn();
|
||||||
|
});
|
||||||
|
return <Input ref={_ref}>test</Input>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Input({ ref }) {
|
||||||
|
let input;
|
||||||
|
didMount(() => {
|
||||||
|
ref({ fn, input });
|
||||||
|
});
|
||||||
|
return <input ref={input} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(ref.input).toBeInstanceOf(HTMLInputElement);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('composition', () => {
|
||||||
|
it('should update prop', ({ container }) => {
|
||||||
|
let update: (name: string) => void;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let name = 'child';
|
||||||
|
update = (val: string) => {
|
||||||
|
name = val;
|
||||||
|
};
|
||||||
|
return <Child name={name} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Child({ name }: { name: string }) {
|
||||||
|
return <div>name is {name}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>name is child</div>');
|
||||||
|
update('new');
|
||||||
|
expect(container.innerHTML).toBe('<div>name is new</div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('nested component', () => {
|
||||||
|
it('should render sub component using parent state', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
function Heading() {
|
||||||
|
return <h1>{count}</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Heading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<h1>0</h1>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update nested component when parent state changes', ({ container }) => {
|
||||||
|
let setCount: (n: number) => void;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let count = 0;
|
||||||
|
setCount = (n: number) => {
|
||||||
|
count = n;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Counter() {
|
||||||
|
return <div>Count: {count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Counter />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Count: 0</div>');
|
||||||
|
|
||||||
|
setCount(5);
|
||||||
|
expect(container.innerHTML).toBe('<div>Count: 5</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass props through multiple levels of nesting', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let name = 'Alice';
|
||||||
|
|
||||||
|
function Parent({ children }) {
|
||||||
|
return (
|
||||||
|
<div className="parent">
|
||||||
|
<h2>Parent</h2>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Child({ name }: { name: string }) {
|
||||||
|
return <div className="child">Hello, {name}!</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Parent>
|
||||||
|
<Child name={name} />
|
||||||
|
</Parent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<div class="parent"><h2>Parent</h2><div class="child">Hello, Alice!</div></div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle sibling nested components with independent state', ({ container }) => {
|
||||||
|
let incrementA: () => void;
|
||||||
|
let incrementB: () => void;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let a = 0;
|
||||||
|
let b = 0;
|
||||||
|
incrementA = () => {
|
||||||
|
a += 1;
|
||||||
|
};
|
||||||
|
incrementB = () => {
|
||||||
|
b += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Counter({ name }: { name: string }) {
|
||||||
|
const value = name === 'A' ? a : b;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Counter {name}: {value}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Counter name="A" />
|
||||||
|
<Counter name="B" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div><div>Counter A: 0</div><div>Counter B: 0</div></div>');
|
||||||
|
|
||||||
|
incrementA();
|
||||||
|
expect(container.innerHTML).toBe('<div><div>Counter A: 1</div><div>Counter B: 0</div></div>');
|
||||||
|
|
||||||
|
incrementB();
|
||||||
|
expect(container.innerHTML).toBe('<div><div>Counter A: 1</div><div>Counter B: 1</div></div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,820 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, vi } from 'vitest';
|
||||||
|
import { domTest as it } from './utils';
|
||||||
|
import { render, didMount } from '../src';
|
||||||
|
|
||||||
|
vi.mock('../src/scheduler', async () => {
|
||||||
|
return {
|
||||||
|
schedule: (task: () => void) => {
|
||||||
|
task();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Computed Properties', () => {
|
||||||
|
describe('Basic Functionality', () => {
|
||||||
|
it('should correctly compute a value based on a single dependency', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let count = 0;
|
||||||
|
const doubleCount = count * 2;
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
count = count + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">{doubleCount}</p>
|
||||||
|
<button onClick={onClick}>Increment</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('0');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update computed value when dependency changes', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let width = 5;
|
||||||
|
let height = 10;
|
||||||
|
const area = width * height;
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
width = width + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">{area}</p>
|
||||||
|
<button onClick={onClick}>Increase Width</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('50');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('60');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly compute and render a derived string state', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let firstName = 'John';
|
||||||
|
let lastName = 'Doe';
|
||||||
|
const fullName = `${firstName} ${lastName}`;
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateName() {
|
||||||
|
firstName = 'Jane';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">{fullName}</p>
|
||||||
|
<button onClick={updateName}>Update Name</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('John Doe');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Jane Doe');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly compute and render a derived number state', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let price = 10;
|
||||||
|
let quantity = 2;
|
||||||
|
const total = price * quantity;
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function increaseQuantity() {
|
||||||
|
quantity += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">Total: ${total}</p>
|
||||||
|
<button onClick={increaseQuantity}>Add Item</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('Total: $20');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Total: $30');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly compute and render a derived boolean state', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let age = 17;
|
||||||
|
const isAdult = age >= 18;
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function increaseAge() {
|
||||||
|
age += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">Is Adult: {isAdult ? 'Yes' : 'No'}</p>
|
||||||
|
<button onClick={increaseAge}>Have Birthday</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('Is Adult: No');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Is Adult: Yes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly compute and render a derived array state', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let numbers = [1, 2, 3, 4, 5];
|
||||||
|
const evenNumbers = numbers.filter(n => n % 2 === 0);
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function addNumber() {
|
||||||
|
numbers.push(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">Even numbers: {evenNumbers.join(', ')}</p>
|
||||||
|
<button onClick={addNumber}>Add Number</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('Even numbers: 2, 4');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Even numbers: 2, 4, 6');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly compute and render a derived object state', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let user = { name: 'John', age: 30 };
|
||||||
|
const userSummary = { ...user, isAdult: user.age >= 18 };
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateAge() {
|
||||||
|
user.age = 17;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">
|
||||||
|
{userSummary.name} is {userSummary.isAdult ? 'an adult' : 'not an adult'}
|
||||||
|
</p>
|
||||||
|
<button onClick={updateAge}>Update Age</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('John is an adult');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('John is not an adult');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly compute state based on array index', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let items = ['Apple', 'Banana', 'Cherry'];
|
||||||
|
let index = 0;
|
||||||
|
const currentItem = items[index];
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function nextItem() {
|
||||||
|
index = (index + 1) % items.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">Current item: {currentItem}</p>
|
||||||
|
<button onClick={nextItem}>Next Item</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('Current item: Apple');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Current item: Banana');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Current item: Cherry');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Current item: Apple');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Multiple Dependencies', () => {
|
||||||
|
it('should compute correctly with multiple dependencies', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let x = 5;
|
||||||
|
let y = 10;
|
||||||
|
let z = 2;
|
||||||
|
const result = x * y + z;
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
return <p data-testid="result">{result}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('52');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.fails('should compute correctly with functional dependencies', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let x = 1;
|
||||||
|
const double = x * 2;
|
||||||
|
const quadruple = double * 2;
|
||||||
|
const getQuadruple = () => quadruple;
|
||||||
|
const result = getQuadruple() + x;
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(container.innerHTML).toBe('<div>5</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update when any dependency changes', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let x = 5;
|
||||||
|
let y = 10;
|
||||||
|
const sum = x + y;
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onClickX() {
|
||||||
|
x = x + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickY() {
|
||||||
|
y = y + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">{sum}</p>
|
||||||
|
<button onClick={onClickX}>Increase X</button>
|
||||||
|
<button onClick={onClickY}>Increase Y</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('15');
|
||||||
|
container.querySelectorAll('button')[0].click();
|
||||||
|
expect(resultElement.textContent).toBe('16');
|
||||||
|
container.querySelectorAll('button')[1].click();
|
||||||
|
expect(resultElement.textContent).toBe('17');
|
||||||
|
});
|
||||||
|
it('Should correctly compute and render a derived string state from multi dependency', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let firstName = 'John';
|
||||||
|
let lastName = 'Doe';
|
||||||
|
let title = 'Mr.';
|
||||||
|
const fullName = `${title} ${firstName} ${lastName}`;
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateName() {
|
||||||
|
firstName = 'Jane';
|
||||||
|
title = 'Ms.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">{fullName}</p>
|
||||||
|
<button onClick={updateName}>Update Name</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('Mr. John Doe');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Ms. Jane Doe');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly compute and render a derived number state from multi dependency', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let length = 5;
|
||||||
|
let width = 3;
|
||||||
|
let height = 2;
|
||||||
|
const volume = length * width * height;
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateDimensions() {
|
||||||
|
length += 1;
|
||||||
|
width += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">Volume: {volume}</p>
|
||||||
|
<button onClick={updateDimensions}>Update Dimensions</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('Volume: 30');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Volume: 60');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly compute and render a derived boolean state from multi dependency', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let age = 20;
|
||||||
|
let hasLicense = false;
|
||||||
|
let hasCar = true;
|
||||||
|
const canDrive = age >= 18 && hasLicense && hasCar;
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateStatus() {
|
||||||
|
hasLicense = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">Can Drive: {canDrive ? 'Yes' : 'No'}</p>
|
||||||
|
<button onClick={updateStatus}>Get License</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('Can Drive: No');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Can Drive: Yes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly compute and render a derived array state from multi dependency', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let numbers1 = [1, 2, 3];
|
||||||
|
let numbers2 = [4, 5, 6];
|
||||||
|
let filterEven = true;
|
||||||
|
const result = [...numbers1, ...numbers2].filter(n => (filterEven ? n % 2 === 0 : n % 2 !== 0));
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleFilter() {
|
||||||
|
filterEven = !filterEven;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">Filtered numbers: {result.join(', ')}</p>
|
||||||
|
<button onClick={toggleFilter}>Toggle Filter</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('Filtered numbers: 2, 4, 6');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Filtered numbers: 1, 3, 5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly compute and render a derived object state from multi dependency', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let user = { name: 'John', age: 30 };
|
||||||
|
let settings = { theme: 'dark', fontSize: 14 };
|
||||||
|
let isLoggedIn = true;
|
||||||
|
const userProfile = {
|
||||||
|
...user,
|
||||||
|
...settings,
|
||||||
|
status: isLoggedIn ? 'Online' : 'Offline',
|
||||||
|
};
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateStatus() {
|
||||||
|
isLoggedIn = false;
|
||||||
|
settings.theme = 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">
|
||||||
|
{userProfile.name} ({userProfile.age}) - {userProfile.status} - Theme: {userProfile.theme}
|
||||||
|
</p>
|
||||||
|
<button onClick={updateStatus}>Logout</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('John (30) - Online - Theme: dark');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('John (30) - Offline - Theme: light');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Advanced Computed States', () => {
|
||||||
|
it('Should support basic arithmetic operations', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let a = 10;
|
||||||
|
let b = 5;
|
||||||
|
const sum = a + b;
|
||||||
|
const difference = a - b;
|
||||||
|
const product = a * b;
|
||||||
|
const quotient = a / b;
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="result">
|
||||||
|
<p>Sum: {sum}</p>
|
||||||
|
<p>Difference: {difference}</p>
|
||||||
|
<p>Product: {product}</p>
|
||||||
|
<p>Quotient: {quotient}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.innerHTML).toBe('<p>Sum: 15</p><p>Difference: 5</p><p>Product: 50</p><p>Quotient: 2</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should support array indexing', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let arr = [10, 20, 30, 40, 50];
|
||||||
|
let index = 2;
|
||||||
|
const value = arr[index];
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateIndex() {
|
||||||
|
index = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">Value: {value}</p>
|
||||||
|
<button onClick={updateIndex}>Update Index</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('Value: 30');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Value: 50');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should support property access', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let obj = { name: 'John', age: 30 };
|
||||||
|
const name = obj.name;
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateName() {
|
||||||
|
obj.name = 'Jane';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">Name: {name}</p>
|
||||||
|
<button onClick={updateName}>Update Name</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('Name: John');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Name: Jane');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should support function calls', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let numbers = [1, 2, 3, 4, 5];
|
||||||
|
const sum = numbers.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
return <p data-testid="result">Sum: {sum}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('Sum: 15');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should support various number operations', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let num = 3.14159;
|
||||||
|
const rounded = Math.round(num);
|
||||||
|
const floored = Math.floor(num);
|
||||||
|
const ceiled = Math.ceil(num);
|
||||||
|
const squared = Math.pow(num, 2);
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="result">
|
||||||
|
<p>Rounded: {rounded}</p>
|
||||||
|
<p>Floored: {floored}</p>
|
||||||
|
<p>Ceiled: {ceiled}</p>
|
||||||
|
<p>Squared: {squared.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.innerHTML).toBe('<p>Rounded: 3</p><p>Floored: 3</p><p>Ceiled: 4</p><p>Squared: 9.87</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should support map operations', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let numbers = [1, 2, 3, 4, 5];
|
||||||
|
const squaredNumbers = numbers.map(n => n * n);
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
return <p data-testid="result">Squared: {squaredNumbers.join(', ')}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('Squared: 1, 4, 9, 16, 25');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should support conditional expressions', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let age = 20;
|
||||||
|
let hasLicense = true;
|
||||||
|
const canDrive = age >= 18 ? (hasLicense ? 'Yes' : 'No, needs license') : 'No, too young';
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateAge() {
|
||||||
|
age = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">Can Drive: {canDrive}</p>
|
||||||
|
<button onClick={updateAge}>Update Age</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('Can Drive: Yes');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('Can Drive: No, too young');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Nested Computed Properties', () => {
|
||||||
|
it('should handle nested computed properties correctly', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let base = 5;
|
||||||
|
const square = base * base;
|
||||||
|
const cube = square * base;
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
base = base + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">{cube}</p>
|
||||||
|
<button onClick={onClick}>Increase Base</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
expect(resultElement.textContent).toBe('125');
|
||||||
|
container.querySelector('button')!.click();
|
||||||
|
expect(resultElement.textContent).toBe('216');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Conditional Dependence', () => {
|
||||||
|
it('should handle conditional dependencies correctly', ({ container }) => {
|
||||||
|
let resultElement: HTMLElement;
|
||||||
|
let toggleButton: HTMLElement;
|
||||||
|
let incrementValue1Button: HTMLElement;
|
||||||
|
let incrementValue2Button: HTMLElement;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let useAlternative = false;
|
||||||
|
let value1 = 10;
|
||||||
|
let value2 = 20;
|
||||||
|
|
||||||
|
// This computed property has conditional dependencies
|
||||||
|
const result = useAlternative ? value2 * 2 : value1 * 2;
|
||||||
|
|
||||||
|
didMount(() => {
|
||||||
|
resultElement = container.querySelector('[data-testid="result"]')!;
|
||||||
|
toggleButton = container.querySelector('[data-testid="toggle"]')!;
|
||||||
|
incrementValue1Button = container.querySelector('[data-testid="increment-value1"]')!;
|
||||||
|
incrementValue2Button = container.querySelector('[data-testid="increment-value2"]')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
useAlternative = !useAlternative;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick1 = () => {
|
||||||
|
value1 += 5;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClick2 = () => {
|
||||||
|
value2 += 5;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p data-testid="result">{result}</p>
|
||||||
|
<button data-testid="toggle" onClick={toggle}>
|
||||||
|
Toggle Alternative
|
||||||
|
</button>
|
||||||
|
<button data-testid="increment-value1" onClick={onClick1}>
|
||||||
|
Increment Value1
|
||||||
|
</button>
|
||||||
|
<button data-testid="increment-value2" onClick={onClick2}>
|
||||||
|
Increment Value2
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
|
||||||
|
// Check initial state
|
||||||
|
expect(resultElement.textContent).toBe('20');
|
||||||
|
|
||||||
|
// Switch to alternative value
|
||||||
|
toggleButton.click();
|
||||||
|
expect(resultElement.textContent).toBe('40');
|
||||||
|
|
||||||
|
// Increment value1 (should not affect result as we're using value2)
|
||||||
|
incrementValue1Button.click();
|
||||||
|
expect(resultElement.textContent).toBe('40');
|
||||||
|
|
||||||
|
// Increment value2 (should affect result)
|
||||||
|
incrementValue2Button.click();
|
||||||
|
expect(resultElement.textContent).toBe('50');
|
||||||
|
|
||||||
|
// Switch back to original value
|
||||||
|
toggleButton.click();
|
||||||
|
expect(resultElement.textContent).toBe('30'); // value1 was incremented earlier
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,94 +1,265 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
*
|
*
|
||||||
* openInula is licensed under Mulan PSL v2.
|
* openInula is licensed under Mulan PSL v2.
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
*
|
*
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
*
|
*
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, vi } from 'vitest';
|
import { describe, expect, vi } from 'vitest';
|
||||||
import { domTest as it } from './utils';
|
import { domTest as it } from './utils';
|
||||||
import { render, View } from '../src';
|
import { render } from '../src';
|
||||||
|
|
||||||
vi.mock('../src/scheduler', async () => {
|
vi.mock('../src/scheduler', async () => {
|
||||||
return {
|
return {
|
||||||
schedule: (task: () => void) => {
|
schedule: (task: () => void) => {
|
||||||
task();
|
task();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('conditional rendering', () => {
|
describe('conditional rendering', () => {
|
||||||
it('should if, else, else if', ({ container }) => {
|
it('should if, else, else if', ({ container }) => {
|
||||||
let set: (num: number) => void;
|
let set: (num: number) => void;
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
let count = 2;
|
let count = 2;
|
||||||
willMount: {
|
set = (val: number) => {
|
||||||
set = (val: number) => {
|
count = val;
|
||||||
count = val;
|
};
|
||||||
};
|
return (
|
||||||
}
|
<>
|
||||||
return (
|
<if cond={count > 1}>{count} is bigger than is 1</if>
|
||||||
<>
|
<else-if cond={count === 1}>{count} is equal to 1</else-if>
|
||||||
<if cond={count > 1}>{count} is bigger than is 1</if>
|
<else>{count} is smaller than 1</else>
|
||||||
<else-if cond={count === 1}>{count} is equal to 1</else-if>
|
</>
|
||||||
<else>{count} is smaller than 1</else>
|
);
|
||||||
</>
|
}
|
||||||
);
|
|
||||||
}
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('2 is bigger than is 1');
|
||||||
render(App, container);
|
set(1);
|
||||||
expect(container.innerHTML).toBe('2 is bigger than is 1');
|
expect(container.innerHTML).toBe('1 is equal to 1');
|
||||||
set(1);
|
set(0);
|
||||||
expect(container.innerHTML).toBe('1 is equal to 1');
|
expect(container.innerHTML).toBe('0 is smaller than 1');
|
||||||
set(0);
|
});
|
||||||
expect(container.innerHTML).toBe('0 is smaller than 1');
|
|
||||||
});
|
it('should support nested if', ({ container }) => {
|
||||||
|
let set: (num: number) => void;
|
||||||
it('should support nested if', ({ container }) => {
|
|
||||||
let set: (num: number) => void;
|
function App() {
|
||||||
|
let count = 0;
|
||||||
function App() {
|
set = (val: number) => {
|
||||||
let count = 0;
|
count = val;
|
||||||
willMount: {
|
};
|
||||||
set = (val: number) => {
|
return (
|
||||||
count = val;
|
<if cond={count > 1}>
|
||||||
};
|
{count} is bigger than is 1
|
||||||
}
|
<if cond={count > 2}>
|
||||||
return (
|
<div>{count} is bigger than is 2</div>
|
||||||
<if cond={count > 1}>
|
</if>
|
||||||
{count} is bigger than is 1
|
</if>
|
||||||
<if cond={count > 2}>
|
);
|
||||||
<div>{count} is bigger than is 2</div>
|
}
|
||||||
</if>
|
|
||||||
</if>
|
render(App, container);
|
||||||
);
|
expect(container.innerHTML).toMatchInlineSnapshot(`""`);
|
||||||
}
|
set(2);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
||||||
render(App, container);
|
"2 is bigger than is 1"
|
||||||
expect(container.innerHTML).toMatchInlineSnapshot(`""`);
|
`);
|
||||||
set(2);
|
set(3);
|
||||||
expect(container.innerHTML).toMatchInlineSnapshot(`
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
||||||
"2 is bigger than is 1
|
"3 is bigger than is 1<div>3 is bigger than is 2</div>"
|
||||||
"
|
`);
|
||||||
`);
|
set(2);
|
||||||
set(3);
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
||||||
expect(container.innerHTML).toMatchInlineSnapshot(`
|
"2 is bigger than is 1"
|
||||||
"3 is bigger than is 1
|
`);
|
||||||
<div>3 is bigger than is 2</div>"
|
});
|
||||||
`);
|
it('should transform "and expression in and expression" to "if tag in if tag"', ({ container }) => {
|
||||||
set(2);
|
let set: (num: number) => void;
|
||||||
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
||||||
"2 is bigger than is 1
|
function MyComp() {
|
||||||
"
|
let count = 0;
|
||||||
`);
|
set = (val: number) => {
|
||||||
});
|
count = val;
|
||||||
});
|
};
|
||||||
|
return <div>{count > 0 && count > 1 && <h1>hello world {count}</h1>}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(MyComp, container);
|
||||||
|
set(0);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<div></div>"`);
|
||||||
|
set(1);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<div></div>"`);
|
||||||
|
set(2);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<div><h1>hello world 2</h1></div>"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not transform "and expression" whose right is identifier', ({ container }) => {
|
||||||
|
let set: (num: number) => void;
|
||||||
|
|
||||||
|
function MyComp() {
|
||||||
|
let count = 0;
|
||||||
|
set = val => {
|
||||||
|
count = val;
|
||||||
|
};
|
||||||
|
return <div>{count > 0 && count > 1 ? <h1>hello world {count}</h1> : `Empty`}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(MyComp, container);
|
||||||
|
set(0);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<div>Empty</div>"`);
|
||||||
|
set(1);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<div>Empty</div>"`);
|
||||||
|
set(2);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<div><h1>hello world 2</h1></div>"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform "condition expression in condition expression" to "if else tag in if else tag"', ({
|
||||||
|
container,
|
||||||
|
}) => {
|
||||||
|
let set: (num: number) => void;
|
||||||
|
|
||||||
|
function MyComp() {
|
||||||
|
let count = 0;
|
||||||
|
set = val => {
|
||||||
|
count = val;
|
||||||
|
};
|
||||||
|
return <div>{count > 0 ? count > 1 ? <h1>hello world {count}</h1> : 'Empty2' : 'Empty1'}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(MyComp, container);
|
||||||
|
set(0);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<div>Empty1</div>"`);
|
||||||
|
set(1);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<div>Empty2</div>"`);
|
||||||
|
set(2);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<div><h1>hello world 2</h1></div>"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('additional conditional rendering tests', () => {
|
||||||
|
it('Should correctly render content based on a single if condition', ({ container }) => {
|
||||||
|
let set: (num: number) => void;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let count = 0;
|
||||||
|
set = (val: number) => {
|
||||||
|
count = val;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<if cond={count > 5}>Count is greater than 5</if>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div></div>');
|
||||||
|
set(6);
|
||||||
|
expect(container.innerHTML).toBe('<div>Count is greater than 5</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly render content based on if-else conditions', ({ container }) => {
|
||||||
|
let set: (num: number) => void;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let count = 0;
|
||||||
|
set = (val: number) => {
|
||||||
|
count = val;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<if cond={count % 2 === 0}>Count is even</if>
|
||||||
|
<else>Count is odd</else>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Count is even</div>');
|
||||||
|
set(1);
|
||||||
|
expect(container.innerHTML).toBe('<div>Count is odd</div>');
|
||||||
|
set(2);
|
||||||
|
expect(container.innerHTML).toBe('<div>Count is even</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly render content based on if-else-if-else conditions', ({ container }) => {
|
||||||
|
let set: (num: number) => void;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let count = 0;
|
||||||
|
set = (val: number) => {
|
||||||
|
count = val;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<if cond={count > 10}>Count is greater than 10</if>
|
||||||
|
<else-if cond={count > 5}>Count is greater than 5 but not greater than 10</else-if>
|
||||||
|
<else-if cond={count > 0}>Count is greater than 0 but not greater than 5</else-if>
|
||||||
|
<else>Count is 0 or negative</else>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Count is 0 or negative</div>');
|
||||||
|
set(3);
|
||||||
|
expect(container.innerHTML).toBe('<div>Count is greater than 0 but not greater than 5</div>');
|
||||||
|
set(7);
|
||||||
|
expect(container.innerHTML).toBe('<div>Count is greater than 5 but not greater than 10</div>');
|
||||||
|
set(11);
|
||||||
|
expect(container.innerHTML).toBe('<div>Count is greater than 10</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly handle nested conditional rendering', ({ container }) => {
|
||||||
|
let set: (obj: { x: number; y: number }) => void;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let state = { x: 0, y: 0 };
|
||||||
|
set = (val: { x: number; y: number }) => {
|
||||||
|
state = val;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<if cond={state.x > 0}>
|
||||||
|
X is positive
|
||||||
|
<if cond={state.y > 0}>
|
||||||
|
<div>Both X and Y are positive</div>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<div>X is positive but Y is not</div>
|
||||||
|
</else>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
X is not positive
|
||||||
|
<if cond={state.y > 0}>
|
||||||
|
<div>X is not positive but Y is</div>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<div>Neither X nor Y are positive</div>
|
||||||
|
</else>
|
||||||
|
</else>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>X is not positive<div>Neither X nor Y are positive</div></div>');
|
||||||
|
set({ x: 1, y: 0 });
|
||||||
|
expect(container.innerHTML).toBe('<div>X is positive<div>X is positive but Y is not</div></div>');
|
||||||
|
set({ x: 1, y: 1 });
|
||||||
|
expect(container.innerHTML).toBe('<div>X is positive<div>Both X and Y are positive</div></div>');
|
||||||
|
set({ x: 0, y: 1 });
|
||||||
|
expect(container.innerHTML).toBe('<div>X is not positive<div>X is not positive but Y is</div></div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,349 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, vi } from 'vitest';
|
||||||
|
import { domTest as it } from './utils';
|
||||||
|
import { render } from '../src';
|
||||||
|
import { createContext, useContext } from '../src/ContextNode';
|
||||||
|
|
||||||
|
vi.mock('../src/scheduler', async () => {
|
||||||
|
return {
|
||||||
|
schedule: (task: () => void) => {
|
||||||
|
task();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('context', () => {
|
||||||
|
it('should support context', ({ container }) => {
|
||||||
|
const TheContext = createContext({
|
||||||
|
theme: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<TheContext theme="dark">
|
||||||
|
<Child name="child" />
|
||||||
|
</TheContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Child({ name }) {
|
||||||
|
const { theme } = useContext(TheContext);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
name is {name}, theme is {theme}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML.trim()).toBe('name is child, theme is dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support recursive context', ({ container }) => {
|
||||||
|
const FileContext = createContext({
|
||||||
|
level: 0,
|
||||||
|
});
|
||||||
|
// Folder is the consumer and provider at same time
|
||||||
|
const Folder = ({ name, children }) => {
|
||||||
|
const { level } = useContext(FileContext);
|
||||||
|
return (
|
||||||
|
<FileContext level={level + 1}>
|
||||||
|
<div>
|
||||||
|
<h1>{`Folder: ${name}, level: ${level}`}</h1>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</FileContext>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const File = ({ name }) => {
|
||||||
|
const { level } = useContext(FileContext);
|
||||||
|
|
||||||
|
return <div>{`File: ${name}, level: ${level}`}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Folder name="Root">
|
||||||
|
<File name="file1.txt" />
|
||||||
|
<Folder name="Subfolder 2">
|
||||||
|
<File name="file2.txt" />
|
||||||
|
</Folder>
|
||||||
|
</Folder>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1>
|
||||||
|
Folder: Root, level: 0
|
||||||
|
</h1>
|
||||||
|
<div>
|
||||||
|
File: file1.txt, level: 1
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1>
|
||||||
|
Folder: Subfolder 2, level: 1
|
||||||
|
</h1>
|
||||||
|
<div>
|
||||||
|
File: file2.txt, level: 2
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support use context in conditional node', ({ container }) => {
|
||||||
|
const ThemeContext = createContext('light');
|
||||||
|
|
||||||
|
function Child({ name }) {
|
||||||
|
const { theme } = useContext(ThemeContext);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>{name}</h1>
|
||||||
|
<div>{theme}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let showChild;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let show = false;
|
||||||
|
showChild = () => {
|
||||||
|
show = true;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ThemeContext theme="dark">
|
||||||
|
<if cond={show}>
|
||||||
|
<Child name="True branch" />
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<Child name="False branch" />
|
||||||
|
</else>
|
||||||
|
</ThemeContext>
|
||||||
|
<ThemeContext theme="light">
|
||||||
|
<Child name="Side branch" />
|
||||||
|
</ThemeContext>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<h1>False branch</h1><div>dark</div><h1>Side branch</h1><div>light</div>"`
|
||||||
|
);
|
||||||
|
showChild();
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<h1>True branch</h1><div>dark</div><h1>Side branch</h1><div>light</div>"`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support use context in loop node', ({ container }) => {
|
||||||
|
const ItemContext = createContext(null);
|
||||||
|
const ThemeContext = createContext('light');
|
||||||
|
let addItem;
|
||||||
|
|
||||||
|
function ItemList() {
|
||||||
|
const items = ['Apple', 'Banana'];
|
||||||
|
addItem = item => {
|
||||||
|
items.push(item);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<ThemeContext theme="dark">
|
||||||
|
<ul>
|
||||||
|
<for each={items}>
|
||||||
|
{item => (
|
||||||
|
<ItemContext item={item}>
|
||||||
|
<ListItem />
|
||||||
|
</ItemContext>
|
||||||
|
)}
|
||||||
|
</for>
|
||||||
|
</ul>
|
||||||
|
</ThemeContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListItem() {
|
||||||
|
const { item } = useContext(ItemContext);
|
||||||
|
const { theme } = useContext(ThemeContext);
|
||||||
|
return <li>{`${theme} - ${item}`}</li>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(ItemList, container);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<ul><li>dark - Apple</li><li>dark - Banana</li></ul>"`);
|
||||||
|
addItem('grape');
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<ul><li>dark - Apple</li><li>dark - Banana</li><li>dark - grape</li></ul>"`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update context value', ({ container }) => {
|
||||||
|
const CountContext = createContext(0);
|
||||||
|
|
||||||
|
function Counter() {
|
||||||
|
const { count } = useContext(CountContext);
|
||||||
|
return <div>Count: {count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App({ initialCount = 0 }) {
|
||||||
|
let count = initialCount;
|
||||||
|
const onClick = () => (count = count + 1);
|
||||||
|
return (
|
||||||
|
<CountContext count={count}>
|
||||||
|
<Counter />
|
||||||
|
<button onClick={onClick}>Increment</button>
|
||||||
|
</CountContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.querySelector('div').textContent).toBe('Count: 0');
|
||||||
|
|
||||||
|
container.querySelector('button').click();
|
||||||
|
expect(container.querySelector('div').textContent).toBe('Count: 1');
|
||||||
|
});
|
||||||
|
it('Should correctly create and provide context', ({ container }) => {
|
||||||
|
const ThemeContext = createContext('light');
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<ThemeContext theme="dark">
|
||||||
|
<Child />
|
||||||
|
</ThemeContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Child() {
|
||||||
|
const { theme } = useContext(ThemeContext);
|
||||||
|
return <div>Current theme: {theme}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Current theme: dark</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly consume context in child components', ({ container }) => {
|
||||||
|
const UserContext = createContext({ name: '', age: 0 });
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<UserContext name="Alice" age={30}>
|
||||||
|
<Parent />
|
||||||
|
</UserContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Parent() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Child1 />
|
||||||
|
<Child2 />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Child1() {
|
||||||
|
const { name } = useContext(UserContext);
|
||||||
|
return <div>Name: {name}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Child2() {
|
||||||
|
const { age } = useContext(UserContext);
|
||||||
|
return <div>Age: {age}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div><div>Name: Alice</div><div>Age: 30</div></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly update context and re-render consumers', ({ container }) => {
|
||||||
|
const CountContext = createContext(0);
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let count = 0;
|
||||||
|
const increment = () => {
|
||||||
|
count += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CountContext count={count}>
|
||||||
|
<Counter />
|
||||||
|
<button onClick={increment}>Increment</button>
|
||||||
|
</CountContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Counter() {
|
||||||
|
const { count } = useContext(CountContext);
|
||||||
|
return <div>Count: {count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.querySelector('div').textContent).toBe('Count: 0');
|
||||||
|
|
||||||
|
container.querySelector('button').click();
|
||||||
|
expect(container.querySelector('div').textContent).toBe('Count: 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should handle nested contexts correctly', ({ container }) => {
|
||||||
|
const ThemeContext = createContext('light');
|
||||||
|
const LanguageContext = createContext('en');
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<ThemeContext theme="dark">
|
||||||
|
<LanguageContext language="fr">
|
||||||
|
<Child />
|
||||||
|
</LanguageContext>
|
||||||
|
</ThemeContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Child() {
|
||||||
|
const { theme } = useContext(ThemeContext);
|
||||||
|
const { language } = useContext(LanguageContext);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Theme: {theme}, Language: {language}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Theme: dark, Language: fr</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should use default value when no provider is present', ({ container }) => {
|
||||||
|
const DefaultContext = createContext({ message: 'Default message' });
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <Child />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Child() {
|
||||||
|
const { message } = useContext(DefaultContext);
|
||||||
|
return <div>{message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Default message</div>');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,738 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
import { describe, expect, vi } from 'vitest';
|
||||||
|
import { domTest as it } from './utils';
|
||||||
|
|
||||||
|
import { didMount, didUnMount, render } from '../src';
|
||||||
|
import { useContext, createContext } from '../src/ContextNode';
|
||||||
|
|
||||||
|
// 模拟调度器
|
||||||
|
vi.mock('../src/scheduler', async () => {
|
||||||
|
return {
|
||||||
|
schedule: (task: () => void) => {
|
||||||
|
task();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自定义 Hook
|
||||||
|
function useCounter(initialValue = 0) {
|
||||||
|
let count = initialValue;
|
||||||
|
const setCount = () => {
|
||||||
|
count += 1;
|
||||||
|
};
|
||||||
|
return { count, setCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Custom Hook Tests', () => {
|
||||||
|
// 1. Basic Functionality
|
||||||
|
describe('Basic Functionality', () => {
|
||||||
|
it('should initialize with the correct value', ({ container }) => {
|
||||||
|
function TestComponent() {
|
||||||
|
const { count } = useCounter(5);
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>5</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update state correctly', ({ container }) => {
|
||||||
|
let setCountFromHook: () => void;
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const { count, setCount } = useCounter();
|
||||||
|
setCountFromHook = setCount;
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>0</div>');
|
||||||
|
setCountFromHook();
|
||||||
|
expect(container.innerHTML).toBe('<div>1</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Version accepting an object parameter
|
||||||
|
function useCounterWithObject({ initial = 0, step = 1 }) {
|
||||||
|
let count = initial;
|
||||||
|
const setCount = () => {
|
||||||
|
count += step;
|
||||||
|
};
|
||||||
|
return { count, setCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version accepting multiple parameters
|
||||||
|
function useCounterWithMultipleParams(initial = 0, step = 1, max = Infinity) {
|
||||||
|
let count = initial;
|
||||||
|
const setCount = () => {
|
||||||
|
count = Math.min(count + step, max);
|
||||||
|
};
|
||||||
|
return { count, setCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returning an array
|
||||||
|
function useCounterReturningArray(initial = 0) {
|
||||||
|
let count = initial;
|
||||||
|
const setCount = () => {
|
||||||
|
count += 1;
|
||||||
|
};
|
||||||
|
return [count, setCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test object input
|
||||||
|
it('should initialize with an object input', ({ container }) => {
|
||||||
|
function TestComponent() {
|
||||||
|
const { count } = useCounterWithObject({ initial: 10, step: 2 });
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>10</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test multiple variable inputs
|
||||||
|
it('should initialize with multiple variable inputs', ({ container }) => {
|
||||||
|
function TestComponent() {
|
||||||
|
const { count } = useCounterWithMultipleParams(15, 3, 20);
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>15</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support derived by state', ({ container }) => {
|
||||||
|
let updateCount: (max: number) => void;
|
||||||
|
function TestComponent() {
|
||||||
|
let init = 15;
|
||||||
|
updateCount = (value: number) => (init = value);
|
||||||
|
const { count } = useCounterWithMultipleParams(init, 3, 20);
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>15</div>');
|
||||||
|
updateCount(10);
|
||||||
|
expect(container.innerHTML).toBe('<div>10</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test single variable output
|
||||||
|
it('should return a single variable output', ({ container }) => {
|
||||||
|
function TestComponent() {
|
||||||
|
const { count } = useCounter();
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>0</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test object output
|
||||||
|
it('should return an object output', ({ container }) => {
|
||||||
|
let setCountFromHook: () => void;
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const { count, setCount } = useCounter();
|
||||||
|
setCountFromHook = setCount;
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>0</div>');
|
||||||
|
setCountFromHook();
|
||||||
|
expect(container.innerHTML).toBe('<div>1</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test object input and update
|
||||||
|
it('should initialize with an object input and update correctly', ({ container }) => {
|
||||||
|
let incrementFromHook;
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const { count, setCount } = useCounterWithObject({ initial: 10, step: 2 });
|
||||||
|
incrementFromHook = setCount;
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>10</div>');
|
||||||
|
incrementFromHook();
|
||||||
|
expect(container.innerHTML).toBe('<div>12</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test array output
|
||||||
|
it('should return an array output', ({ container }) => {
|
||||||
|
let setCountFromHook: () => void;
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const [count, setCount] = useCounterReturningArray();
|
||||||
|
setCountFromHook = setCount;
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>0</div>');
|
||||||
|
setCountFromHook();
|
||||||
|
expect(container.innerHTML).toBe('<div>1</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test array output and update
|
||||||
|
it('should return an array output and update correctly', ({ container }) => {
|
||||||
|
let incrementFromHook;
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const [count, increment] = useCounterReturningArray(5);
|
||||||
|
incrementFromHook = increment;
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>5</div>');
|
||||||
|
incrementFromHook();
|
||||||
|
expect(container.innerHTML).toBe('<div>6</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test complex variable updates
|
||||||
|
it('should update state correctly with complex inputs', ({ container }) => {
|
||||||
|
let setCountFromHook: () => void;
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const { count, setCount } = useCounterWithMultipleParams(5, 2, 10);
|
||||||
|
setCountFromHook = setCount;
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>5</div>');
|
||||||
|
setCountFromHook();
|
||||||
|
expect(container.innerHTML).toBe('<div>7</div>');
|
||||||
|
setCountFromHook();
|
||||||
|
expect(container.innerHTML).toBe('<div>9</div>');
|
||||||
|
setCountFromHook();
|
||||||
|
expect(container.innerHTML).toBe('<div>10</div>'); // Max value reached
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Multiple Instances
|
||||||
|
describe('Multiple Instances', () => {
|
||||||
|
it('should maintain separate state for multiple instances', ({ container }) => {
|
||||||
|
let setCount1: () => void, setCount2: () => void;
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const { count: count1, setCount: setCount1Hook } = useCounter(0);
|
||||||
|
const { count: count2, setCount: setCount2Hook } = useCounter(10);
|
||||||
|
setCount1 = setCount1Hook;
|
||||||
|
setCount2 = setCount2Hook;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{count1}-{count2}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>0-10</div>');
|
||||||
|
setCount1();
|
||||||
|
setCount2();
|
||||||
|
expect(container.innerHTML).toBe('<div>1-11</div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Lifecycle and Effects
|
||||||
|
describe('Lifecycle and Effects', () => {
|
||||||
|
it.fails('should run effects on mount and cleanup on unmount', ({ container }) => {
|
||||||
|
const mockEffect = vi.fn();
|
||||||
|
const mockCleanup = vi.fn();
|
||||||
|
|
||||||
|
function useEffectTest() {
|
||||||
|
didMount(() => {
|
||||||
|
mockEffect();
|
||||||
|
});
|
||||||
|
didUnMount(() => {
|
||||||
|
mockCleanup();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
useEffectTest();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
|
||||||
|
expect(mockEffect).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Context Integration
|
||||||
|
describe('Context Integration', () => {
|
||||||
|
it('should consume context correctly', ({ container }) => {
|
||||||
|
const CountContext = createContext(0);
|
||||||
|
|
||||||
|
function useContextCounter() {
|
||||||
|
const count = useContext(CountContext);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const { count } = useContextCounter();
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<CountContext count={5}>
|
||||||
|
<TestComponent />
|
||||||
|
</CountContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>5</div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Props Handling
|
||||||
|
describe('Props Handling', () => {
|
||||||
|
it('should update when props change', ({ container }) => {
|
||||||
|
function usePropsTest({ initial }: { initial: number }) {
|
||||||
|
const value = initial;
|
||||||
|
return value * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
let update: (n: number) => void;
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
let value = 5;
|
||||||
|
const hookValue = usePropsTest({ initial: value });
|
||||||
|
update = (n: number) => (value = n);
|
||||||
|
return <div>{hookValue}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>10</div>');
|
||||||
|
update(10);
|
||||||
|
expect(container.innerHTML).toBe('<div>20</div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. Hook Nesting
|
||||||
|
describe('Hook Nesting', () => {
|
||||||
|
it('should handle nested hook calls correctly', ({ container }) => {
|
||||||
|
function useNestedCounter(initial: number) {
|
||||||
|
const { count, setCount } = useCounter(initial);
|
||||||
|
const doubleCount = count * 2;
|
||||||
|
return { count, doubleCount, setCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
let setCountFromHook: () => void;
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const { count, doubleCount, setCount } = useNestedCounter(5);
|
||||||
|
setCountFromHook = setCount;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{count}-{doubleCount}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>5-10</div>');
|
||||||
|
// @ts-ignore
|
||||||
|
setCountFromHook();
|
||||||
|
expect(container.innerHTML).toBe('<div>6-12</div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. Hook Computation
|
||||||
|
describe('hook return value computation', () => {
|
||||||
|
it('should receive props and output value', ({ container }) => {
|
||||||
|
let updateValue: (n: number) => void;
|
||||||
|
|
||||||
|
function usePropsTest({ initial }: { initial: number }) {
|
||||||
|
return initial * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
let value = 1;
|
||||||
|
const hookValue = usePropsTest({ initial: value });
|
||||||
|
let computed = hookValue * 2;
|
||||||
|
updateValue = n => (value = n);
|
||||||
|
return <div>{computed}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>4</div>');
|
||||||
|
updateValue(2);
|
||||||
|
expect(container.innerHTML).toBe('<div>8</div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Hook and Watch Combined Tests', () => {
|
||||||
|
it('should update watched value when hook state changes', ({ container }) => {
|
||||||
|
let setCount;
|
||||||
|
function useCounter(initial = 0) {
|
||||||
|
let count = initial;
|
||||||
|
setCount = n => {
|
||||||
|
count = n;
|
||||||
|
};
|
||||||
|
return { count };
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const { count } = useCounter(0);
|
||||||
|
let watchedCount = 0;
|
||||||
|
|
||||||
|
watch(() => {
|
||||||
|
watchedCount = count * 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div>{watchedCount}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>0</div>');
|
||||||
|
setCount(5);
|
||||||
|
expect(container.innerHTML).toBe('<div>10</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple watches in a custom hook', ({ container }) => {
|
||||||
|
let setX, setY;
|
||||||
|
function usePosition() {
|
||||||
|
let x = 0,
|
||||||
|
y = 0;
|
||||||
|
setX = newX => {
|
||||||
|
x = newX;
|
||||||
|
};
|
||||||
|
setY = newY => {
|
||||||
|
y = newY;
|
||||||
|
};
|
||||||
|
|
||||||
|
let position = '';
|
||||||
|
watch(() => {
|
||||||
|
position = `(${x},${y})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
let quadrant = 0;
|
||||||
|
watch(() => {
|
||||||
|
quadrant = x >= 0 && y >= 0 ? 1 : x < 0 && y >= 0 ? 2 : x < 0 && y < 0 ? 3 : 4;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { position, quadrant };
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const { position, quadrant } = usePosition();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{position} Q{quadrant}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>(0,0) Q1</div>');
|
||||||
|
setX(-5);
|
||||||
|
setY(10);
|
||||||
|
expect(container.innerHTML).toBe('<div>(-5,10) Q2</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly handle watch dependencies in hooks', ({ container }) => {
|
||||||
|
let setItems;
|
||||||
|
function useFilteredList(initialItems = []) {
|
||||||
|
let items = initialItems;
|
||||||
|
setItems = newItems => {
|
||||||
|
items = newItems;
|
||||||
|
};
|
||||||
|
|
||||||
|
let evenItems = [];
|
||||||
|
let oddItems = [];
|
||||||
|
|
||||||
|
watch(() => {
|
||||||
|
evenItems = items.filter(item => item % 2 === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => {
|
||||||
|
oddItems = items.filter(item => item % 2 !== 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { evenItems, oddItems };
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const { evenItems, oddItems } = useFilteredList([1, 2, 3, 4, 5]);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Even: {evenItems.join(',')} Odd: {oddItems.join(',')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Even: 2,4 Odd: 1,3,5</div>');
|
||||||
|
setItems([2, 4, 6, 8, 10]);
|
||||||
|
expect(container.innerHTML).toBe('<div>Even: 2,4,6,8,10 Odd: </div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Advanced Hook Tests', () => {
|
||||||
|
// Hook return tests
|
||||||
|
describe('Hook Return Tests', () => {
|
||||||
|
it('should handle expression return', ({ container }) => {
|
||||||
|
function useExpression(a: number, b: number) {
|
||||||
|
return a + b * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const result = useExpression(3, 4);
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>11</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle object spread return', ({ container }) => {
|
||||||
|
function useObjectSpread(obj: object) {
|
||||||
|
return { ...obj, newProp: 'added' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const result = useObjectSpread({ existingProp: 'original' });
|
||||||
|
return <div>{JSON.stringify(result)}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>{"existingProp":"original","newProp":"added"}</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle function call return', ({ container }) => {
|
||||||
|
function useFunction() {
|
||||||
|
const innerFunction = () => 42;
|
||||||
|
return innerFunction();
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const result = useFunction();
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>42</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle conditional expression return', ({ container }) => {
|
||||||
|
function useConditional(condition: boolean) {
|
||||||
|
return condition ? 'True' : 'False';
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const result = useConditional(true);
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>True</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle array computation return', ({ container }) => {
|
||||||
|
function useArrayComputation(arr: number[]) {
|
||||||
|
return arr.reduce((sum, num) => sum + num, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const result = useArrayComputation([1, 2, 3, 4, 5]);
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>15</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle ternary expression return', ({ container }) => {
|
||||||
|
function useTernary(value: number) {
|
||||||
|
return value > 5 ? 'High' : value < 0 ? 'Low' : 'Medium';
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const result = useTernary(7);
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>High</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle member expression return', ({ container }) => {
|
||||||
|
function useMemberExpression(obj: { prop: string }) {
|
||||||
|
return obj.prop;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const result = useMemberExpression({ prop: 'test' });
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>test</div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hook input tests
|
||||||
|
describe('Hook Input Tests', () => {
|
||||||
|
it('should handle expression input', ({ container }) => {
|
||||||
|
function useExpression(value: number) {
|
||||||
|
return value * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const result = useExpression(3 + 4);
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>14</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle object spread input', ({ container }) => {
|
||||||
|
function useObjectSpread(obj: { a: number; b: number }) {
|
||||||
|
return obj.a + obj.b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const baseObj = { a: 1, c: 3 };
|
||||||
|
const result = useObjectSpread({ ...baseObj, b: 2 });
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>3</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle function call input', ({ container }) => {
|
||||||
|
function useFunction(value: number) {
|
||||||
|
return value * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValue() {
|
||||||
|
return 21;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const result = useFunction(getValue());
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>42</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle conditional expression input', ({ container }) => {
|
||||||
|
function useConditional(value: string) {
|
||||||
|
return `Received: ${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const condition = true;
|
||||||
|
const result = useConditional(condition ? 'Yes' : 'No');
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Received: Yes</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle array computation input', ({ container }) => {
|
||||||
|
function useArraySum(sum: number) {
|
||||||
|
return `Sum: ${sum}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const numbers = [1, 2, 3, 4, 5];
|
||||||
|
const result = useArraySum(numbers.reduce((sum, num) => sum + num, 0));
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Sum: 15</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle ternary expression input', ({ container }) => {
|
||||||
|
function useStatus(status: string) {
|
||||||
|
return `Current status: ${status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const value = 7;
|
||||||
|
const result = useStatus(value > 5 ? 'High' : value < 0 ? 'Low' : 'Medium');
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Current status: High</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle member expression input', ({ container }) => {
|
||||||
|
function useName(name: string) {
|
||||||
|
return `Hello, ${name}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const user = { name: 'Alice' };
|
||||||
|
const result = useName(user.name);
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Hello, Alice!</div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Additional tests
|
||||||
|
describe('Additional Hook Tests', () => {
|
||||||
|
it('should handle input based on other variables', ({ container }) => {
|
||||||
|
function useComputed(value: number) {
|
||||||
|
return value * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
let baseValue = 5;
|
||||||
|
let multiplier = 3;
|
||||||
|
const result = useComputed(baseValue * multiplier);
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>30</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle function input and output', ({ container }) => {
|
||||||
|
function useFunction(fn: (x: number) => number) {
|
||||||
|
return (y: number) => fn(y) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const inputFn = (x: number) => x + 1;
|
||||||
|
const resultFn = useFunction(inputFn);
|
||||||
|
const result = resultFn(5);
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(TestComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>12</div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,543 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
import { describe, expect, vi } from 'vitest';
|
||||||
|
import { domTest as it } from './utils';
|
||||||
|
import { render } from '../src';
|
||||||
|
|
||||||
|
vi.mock('../src/scheduler', async () => {
|
||||||
|
return {
|
||||||
|
schedule: (task: () => void) => {
|
||||||
|
task();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Event Handling', () => {
|
||||||
|
it('Should correctly handle onClick events', ({ container }) => {
|
||||||
|
let clicked = false;
|
||||||
|
function App() {
|
||||||
|
const handleClick = () => {
|
||||||
|
clicked = true;
|
||||||
|
};
|
||||||
|
return <button onClick={handleClick}>Click me</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
const button = container.querySelector('button');
|
||||||
|
button.click();
|
||||||
|
expect(clicked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.fails('Should correctly handle onMouseOver events', ({ container }) => {
|
||||||
|
let hovered = false;
|
||||||
|
function App() {
|
||||||
|
const handleMouseOver = () => {
|
||||||
|
hovered = true;
|
||||||
|
};
|
||||||
|
return <div onMouseOver={handleMouseOver}>Hover me</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
const div = container.querySelector('div');
|
||||||
|
div.dispatchEvent(new MouseEvent('mouseover'));
|
||||||
|
expect(hovered).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly handle onKeyPress events', ({ container }) => {
|
||||||
|
let keypressed = '';
|
||||||
|
function App() {
|
||||||
|
const handleKeyPress = event => {
|
||||||
|
keypressed = event.key;
|
||||||
|
};
|
||||||
|
return <input onKeyPress={handleKeyPress} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
const input = container.querySelector('input');
|
||||||
|
const event = new KeyboardEvent('keypress', { key: 'A' });
|
||||||
|
input.dispatchEvent(event);
|
||||||
|
expect(keypressed).toBe('A');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly handle onSubmit events', ({ container }) => {
|
||||||
|
let submitted = false;
|
||||||
|
function App() {
|
||||||
|
const handleSubmit = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
submitted = true;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
const form = container.querySelector('form');
|
||||||
|
form.dispatchEvent(new Event('submit'));
|
||||||
|
expect(submitted).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.fails('Should correctly handle custom events', ({ container }) => {
|
||||||
|
let customEventData = null;
|
||||||
|
function App() {
|
||||||
|
const handleCustomEvent = event => {
|
||||||
|
customEventData = event.detail;
|
||||||
|
};
|
||||||
|
return <div onCustomEvent={handleCustomEvent}>Custom event target</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
const div = container.querySelector('div');
|
||||||
|
const customEvent = new CustomEvent('customEvent', { detail: { message: 'Hello, Custom Event!' } });
|
||||||
|
div.dispatchEvent(customEvent);
|
||||||
|
expect(customEventData).toEqual({ message: 'Hello, Custom Event!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly handle events when the handler is a variable', ({ container }) => {
|
||||||
|
let count = 0;
|
||||||
|
function App() {
|
||||||
|
const incrementCount = () => {
|
||||||
|
count++;
|
||||||
|
};
|
||||||
|
return <button onClick={incrementCount}>Increment</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
const button = container.querySelector('button');
|
||||||
|
button.click();
|
||||||
|
expect(count).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly handle events when the handler is an expression returning a function', ({ container }) => {
|
||||||
|
let lastClicked = '';
|
||||||
|
function App() {
|
||||||
|
const createHandler = buttonName => () => {
|
||||||
|
lastClicked = buttonName;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={() => createHandler('Button A')()}>A</button>
|
||||||
|
<button onClick={() => createHandler('Button B')()}>B</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
const buttons = container.querySelectorAll('button');
|
||||||
|
buttons[0].click();
|
||||||
|
expect(lastClicked).toBe('Button A');
|
||||||
|
buttons[1].click();
|
||||||
|
expect(lastClicked).toBe('Button B');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('event emission', () => {
|
||||||
|
it('should handle emit to parent', ({ container }) => {
|
||||||
|
function AnswerButton({ onYes, onNo }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={onYes}>YES</button>
|
||||||
|
|
||||||
|
{/*<button onClick={onNo}>NO</button>*/}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function App() {
|
||||||
|
let isHappy = false;
|
||||||
|
|
||||||
|
function onAnswerNo() {
|
||||||
|
isHappy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAnswerYes() {
|
||||||
|
isHappy = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>Are you happy?</p>
|
||||||
|
<AnswerButton onYes={onAnswerYes} onNo={onAnswerNo} />
|
||||||
|
<p style={{ fontSize: 50 }}>{isHappy ? 'yes' : 'no'}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Are you happy?
|
||||||
|
</p>
|
||||||
|
<button>
|
||||||
|
YES
|
||||||
|
</button>
|
||||||
|
<p>
|
||||||
|
no
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Are you happy?
|
||||||
|
</p>
|
||||||
|
<button>
|
||||||
|
YES
|
||||||
|
</button>
|
||||||
|
<p>
|
||||||
|
yes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
it('should correctly emit events to parent component', ({ container }) => {
|
||||||
|
function Child({ onEvent }) {
|
||||||
|
return <button onClick={() => onEvent('clicked')}>Click me</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let eventReceived = '1';
|
||||||
|
|
||||||
|
function handleEvent(event) {
|
||||||
|
eventReceived = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Child onEvent={handleEvent} />
|
||||||
|
<p>{eventReceived}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<button>
|
||||||
|
Click me
|
||||||
|
</button>
|
||||||
|
<p>
|
||||||
|
1
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<button>
|
||||||
|
Click me
|
||||||
|
</button>
|
||||||
|
<p>
|
||||||
|
clicked
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly update parent state based on emitted events', ({ container }) => {
|
||||||
|
function Counter({ onIncrement, onDecrement }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={onIncrement}>+</button>
|
||||||
|
<button onClick={onDecrement}>-</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
function increment() {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrement() {
|
||||||
|
count -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Counter onIncrement={increment} onDecrement={decrement} />
|
||||||
|
<p>Count:{count}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Count:
|
||||||
|
0
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
container.querySelectorAll('button')[0]?.click();
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Count:
|
||||||
|
1
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
container.querySelectorAll('button')[1]?.click();
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Count:
|
||||||
|
0
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly handle multiple event emissions', ({ container }) => {
|
||||||
|
function MultiButton({ onClickA, onClickB, onClickC }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={onClickA}>A</button>
|
||||||
|
<button onClick={onClickB}>B</button>
|
||||||
|
<button onClick={onClickC}>C</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let lastClicked = 'A';
|
||||||
|
|
||||||
|
function handleClick(button) {
|
||||||
|
lastClicked = button;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MultiButton
|
||||||
|
onClickA={() => handleClick('A')}
|
||||||
|
onClickB={() => handleClick('B')}
|
||||||
|
onClickC={() => handleClick('C')}
|
||||||
|
/>
|
||||||
|
<p>Last clicked:{lastClicked}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button>
|
||||||
|
A
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
B
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
C
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Last clicked:
|
||||||
|
A
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
container.querySelectorAll('button')[1]?.click();
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button>
|
||||||
|
A
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
B
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
C
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Last clicked:
|
||||||
|
B
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle both arrow functions and function variables', ({ container }) => {
|
||||||
|
function Child({ onEventA, onEventB }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={onEventA}>Event A</button>
|
||||||
|
<button onClick={onEventB}>Event B</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let eventResult = '1';
|
||||||
|
|
||||||
|
const handleEventA = () => {
|
||||||
|
eventResult = 'Arrow function called';
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleEventB() {
|
||||||
|
eventResult = 'Function variable called';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Child onEventA={handleEventA} onEventB={handleEventB} />
|
||||||
|
<p>{eventResult}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button>
|
||||||
|
Event A
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
Event B
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
1
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
container.querySelectorAll('button')[0]?.click();
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button>
|
||||||
|
Event A
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
Event B
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Arrow function called
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
container.querySelectorAll('button')[1]?.click();
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button>
|
||||||
|
Event A
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
Event B
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Function variable called
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multi-layer event functions', ({ container }) => {
|
||||||
|
function GrandChild({ onEvent }) {
|
||||||
|
return <button onClick={() => onEvent('GrandChild clicked')}>Click GrandChild</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Child({ onParentEvent }) {
|
||||||
|
function handleChildEvent(message) {
|
||||||
|
onParentEvent(`Child received: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <GrandChild onEvent={handleChildEvent} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let message = '1';
|
||||||
|
|
||||||
|
function handleAppEvent(receivedMessage) {
|
||||||
|
message = `App received: ${receivedMessage}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Child onParentEvent={handleAppEvent} />
|
||||||
|
<p>{message}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<button>
|
||||||
|
Click GrandChild
|
||||||
|
</button>
|
||||||
|
<p>
|
||||||
|
1
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<button>
|
||||||
|
Click GrandChild
|
||||||
|
</button>
|
||||||
|
<p>
|
||||||
|
App received: Child received: GrandChild clicked
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,225 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, vi } from 'vitest';
|
||||||
|
import { domTest as it } from './utils';
|
||||||
|
import { render } from '../src';
|
||||||
|
|
||||||
|
vi.mock('../src/scheduler', async () => {
|
||||||
|
return {
|
||||||
|
schedule: (task: () => void) => {
|
||||||
|
task();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
describe('for', () => {
|
||||||
|
it('should work', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const arr = [0, 1, 2];
|
||||||
|
return <for each={arr}>{item => <div>{item}</div>}</for>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<div>0</div><div>1</div><div>2</div>"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update item when arr changed', ({ container }) => {
|
||||||
|
let updateArr: (num: number) => void;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const arr = [0, 1, 2];
|
||||||
|
updateArr = (num: number) => {
|
||||||
|
arr.push(num);
|
||||||
|
};
|
||||||
|
return <for each={arr}>{item => <div>{item}</div>}</for>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.children.length).toEqual(3);
|
||||||
|
updateArr(3);
|
||||||
|
expect(container.children.length).toEqual(4);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<div>0</div><div>1</div><div>2</div><div>3</div>"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get index', ({ container }) => {
|
||||||
|
let update: (num: number) => void;
|
||||||
|
function App() {
|
||||||
|
const arr = [0, 1, 2];
|
||||||
|
update = (num: number) => {
|
||||||
|
arr.push(num);
|
||||||
|
};
|
||||||
|
return <for each={arr}>{(item, index) => <div>{index}</div>}</for>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<div>0</div><div>1</div><div>2</div>"`);
|
||||||
|
update(3);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<div>0</div><div>1</div><div>2</div><div>3</div>"`);
|
||||||
|
});
|
||||||
|
it('should transform for loop', ({ container }) => {
|
||||||
|
function MyComp() {
|
||||||
|
let name = 'test';
|
||||||
|
let arr = [
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
{ x: 2, y: 2 },
|
||||||
|
{ x: 3, y: 3 },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<for each={arr}>
|
||||||
|
{({ x, y }, index) => {
|
||||||
|
let name1 = 'test';
|
||||||
|
const onClick = () => {
|
||||||
|
name1 = 'changed';
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div onClick={onClick} id={`item${index}`}>
|
||||||
|
{name1}
|
||||||
|
{index}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</for>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(MyComp, container);
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<div id="item0">test0</div><div id="item1">test1</div><div id="item2">test2</div>'
|
||||||
|
);
|
||||||
|
const item = container.querySelector('#item0') as HTMLDivElement;
|
||||||
|
item.click();
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<div id="item0">changed0</div><div id="item1">test1</div><div id="item2">test2</div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform map to for jsx element', ({ container }) => {
|
||||||
|
function MyComp() {
|
||||||
|
const arr = [1, 2, 3];
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{arr.map(item => (
|
||||||
|
<div>{item}</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(MyComp, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>1</div><div>2</div><div>3</div>');
|
||||||
|
});
|
||||||
|
it('should transform map in map to for', ({ container }) => {
|
||||||
|
function MyComp() {
|
||||||
|
const matrix = [
|
||||||
|
[1, 2],
|
||||||
|
[3, 4],
|
||||||
|
];
|
||||||
|
return <div>{matrix.map(arr => arr.map(item => <div>{item}</div>))}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(MyComp, container);
|
||||||
|
expect(container.innerHTML).toBe('<div><div>1</div><div>2</div><div>3</div><div>4</div></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compile error
|
||||||
|
// it.fails('should transform last map to for" ', ({ container }) => {
|
||||||
|
// function MyComp() {
|
||||||
|
// let arr = [1, 2, 3];
|
||||||
|
// return (
|
||||||
|
// <div>
|
||||||
|
// {arr
|
||||||
|
// .map(item => <div>{item}</div>)
|
||||||
|
// .map(item => (
|
||||||
|
// <div>{item}</div>
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// render(MyComp, container);
|
||||||
|
// expect(container.innerHTML).toBe('<div><div>1</div><div>2</div><div>3</div><div>4</div></div>');
|
||||||
|
// });
|
||||||
|
it('Should correctly render a single-level loop of elements', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const fruits = ['Apple', 'Banana', 'Cherry'];
|
||||||
|
return <for each={fruits}>{fruit => <li>{fruit}</li>}</for>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<li>Apple</li><li>Banana</li><li>Cherry</li>"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly render nested loops of elements', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const matrix = [
|
||||||
|
[1, 2],
|
||||||
|
[3, 4],
|
||||||
|
[5, 6],
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<for each={matrix}>
|
||||||
|
{row => (
|
||||||
|
<div>
|
||||||
|
<for each={row}>{cell => <span>{cell}</span>}</for>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</for>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<div><span>1</span><span>2</span></div><div><span>3</span><span>4</span></div><div><span>5</span><span>6</span></div>"`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly render loops with complex data structures', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const users = [
|
||||||
|
{ id: 1, name: 'Alice', hobbies: ['reading', 'gaming'] },
|
||||||
|
{ id: 2, name: 'Bob', hobbies: ['cycling', 'photography'] },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<for each={users}>
|
||||||
|
{user => (
|
||||||
|
<div>
|
||||||
|
<h2>{user.name}</h2>
|
||||||
|
<ul>
|
||||||
|
<for each={user.hobbies}>{hobby => <li>{hobby}</li>}</for>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</for>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<div><h2>Alice</h2><ul><li>reading</li><li>gaming</li></ul></div><div><h2>Bob</h2><ul><li>cycling</li><li>photography</li></ul></div>"`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly render when for tag input is an array map', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const numbers = [1, 2, 3, 4, 5];
|
||||||
|
return <for each={numbers.map(n => n * 2)}>{doubledNumber => <span>{doubledNumber}</span>}</for>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<span>2</span><span>4</span><span>6</span><span>8</span><span>10</span>"`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,209 @@
|
||||||
|
import { describe, expect, vi } from 'vitest';
|
||||||
|
import { domTest as it } from './utils';
|
||||||
|
import { render } from '../src';
|
||||||
|
|
||||||
|
vi.mock('../src/scheduler', async () => {
|
||||||
|
return {
|
||||||
|
schedule: (task: () => void) => {
|
||||||
|
task();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
describe('Fragment Tests', () => {
|
||||||
|
it('should render multiple elements using Fragment', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>First</div>
|
||||||
|
<div>Second</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>First</div><div>Second</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render an empty Fragment', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('');
|
||||||
|
});
|
||||||
|
it('should support nested Fragments', ({ container }) => {
|
||||||
|
function ChildComponent() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span>Child</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>Start</div>
|
||||||
|
<>
|
||||||
|
<p>Nested</p>
|
||||||
|
<ChildComponent />
|
||||||
|
</>
|
||||||
|
<div>End</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Start</div><p>Nested</p><span>Child</span><div>End</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support conditional rendering with Fragments', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let showExtra = false;
|
||||||
|
|
||||||
|
function toggleExtra() {
|
||||||
|
showExtra = !showExtra;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>Always</div>
|
||||||
|
{showExtra && (
|
||||||
|
<>
|
||||||
|
<div>Extra 1</div>
|
||||||
|
<div>Extra 2</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button onClick={toggleExtra}>Toggle</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Always</div><button>Toggle</button>');
|
||||||
|
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container.innerHTML).toBe('<div>Always</div><div>Extra 1</div><div>Extra 2</div><button>Toggle</button>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support ternary operators with Fragments', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let condition = true;
|
||||||
|
|
||||||
|
function toggleCondition() {
|
||||||
|
condition = !condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{condition ? (
|
||||||
|
<>
|
||||||
|
<div>True 1</div>
|
||||||
|
<div>True 2</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div>False 1</div>
|
||||||
|
<div>False 2</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button onClick={toggleCondition}>Toggle</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>True 1</div><div>True 2</div><button>Toggle</button>');
|
||||||
|
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container.innerHTML).toBe('<div>False 1</div><div>False 2</div><button>Toggle</button>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support state updates within Fragments', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
function increment() {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>Count: {count}</div>
|
||||||
|
<button onClick={increment}>Increment</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Count: 0</div><button>Increment</button>');
|
||||||
|
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container.innerHTML).toBe('<div>Count: 1</div><button>Increment</button>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support event handling within Fragments', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let clicked = false;
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={handleClick}>Click me</button>
|
||||||
|
<div>{clicked ? 'Clicked' : 'Not clicked'}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<button>Click me</button><div>Not clicked</div>');
|
||||||
|
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container.innerHTML).toBe('<button>Click me</button><div>Clicked</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not affect CSS styling', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="styled">Styled div</div>
|
||||||
|
<span style={{ color: 'red' }}>Red span</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
const styledDiv = container.querySelector('.styled');
|
||||||
|
const redSpan = container.querySelector('span');
|
||||||
|
|
||||||
|
expect(styledDiv).not.toBeNull();
|
||||||
|
expect(redSpan).not.toBeNull();
|
||||||
|
expect(redSpan?.style.color).toBe('red');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support mixing text and JSX expression containers within Fragments', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const name = 'World';
|
||||||
|
const age = 42;
|
||||||
|
const hobbies = ['reading', 'coding', 'gaming'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
Hello, {name}! You are {age} years old.Your hobbies include:
|
||||||
|
{hobbies.map(h => (
|
||||||
|
<span>{h}</span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'Hello, World! You are 42 years old.Your hobbies include:<span>reading</span><span>coding</span><span>gaming</span>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,680 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, vi } from 'vitest';
|
||||||
|
import { domTest as it } from './utils';
|
||||||
|
import { render } from '../src';
|
||||||
|
import { useContext, createContext } from '../src/ContextNode';
|
||||||
|
|
||||||
|
vi.mock('../src/scheduler', async () => {
|
||||||
|
return {
|
||||||
|
schedule: (task: () => void) => {
|
||||||
|
task();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('JSX Element Usage in Various Contexts', () => {
|
||||||
|
describe('mount', () => {
|
||||||
|
it('should support variable assignment of JSX elements', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const element = <div>Hello, World!</div>;
|
||||||
|
return <>{element}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Hello, World!</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support JSX elements in arrays', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const elements = [<div>First</div>, <div>Second</div>, <div>Third</div>];
|
||||||
|
return <>{elements}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>First</div><div>Second</div><div>Third</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support JSX elements as object properties', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const obj = {
|
||||||
|
header: <h1>Title</h1>,
|
||||||
|
content: <p>Content</p>,
|
||||||
|
footer: <footer>Footer</footer>,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{obj.header}
|
||||||
|
{obj.content}
|
||||||
|
{obj.footer}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<h1>Title</h1><p>Content</p><footer>Footer</footer>');
|
||||||
|
});
|
||||||
|
//
|
||||||
|
// it('should support functions returning JSX elements', ({ container }) => {
|
||||||
|
// function App() {
|
||||||
|
// const getElement = (text: string) => <span>{text}</span>;
|
||||||
|
// return <div>{getElement('Hello')}</div>;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// render(App, container);
|
||||||
|
// expect(container.innerHTML).toBe('<div><span>Hello</span></div>');
|
||||||
|
// });
|
||||||
|
|
||||||
|
it('should support JSX elements in conditional expressions', ({ container }) => {
|
||||||
|
function App({ condition = true }: { condition: boolean }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{condition ? <div>True</div> : <div>False</div>}
|
||||||
|
{condition && <div>Conditional</div>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>True</div><div>Conditional</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support JSX elements as Context Provider values', ({ container }) => {
|
||||||
|
const ThemeContext = createContext(null);
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const theme = <div>Dark Theme</div>;
|
||||||
|
return (
|
||||||
|
<ThemeContext value={theme}>
|
||||||
|
<ThemeConsumer />
|
||||||
|
</ThemeContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ThemeConsumer() {
|
||||||
|
let { value } = useContext(ThemeContext);
|
||||||
|
return <>{value}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Dark Theme</div>');
|
||||||
|
});
|
||||||
|
it('should render string literals', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return <div>{'Hello, World!'}</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Hello, World!</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render numbers', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return <div>{42}</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>42</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.fails('should render booleans (as empty string)', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return <div>{true}</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.fails('should render null and undefined (as empty string)', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{null}
|
||||||
|
{undefined}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.fails('should render arrays of elements', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return <div>{[<span key="1">One</span>, <span key="2">Two</span>]}</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div><span>One</span><span>Two</span></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render function components', ({ container }) => {
|
||||||
|
function Child() {
|
||||||
|
return <span>Child Component</span>;
|
||||||
|
}
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Child />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div><span>Child Component</span></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render fragments', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span>First</span>
|
||||||
|
<span>Second</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<span>First</span><span>Second</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render elements with props', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="test" id="myDiv">
|
||||||
|
Content
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div class="test" id="myDiv">Content</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render elements with children prop', ({ container }) => {
|
||||||
|
function Wrapper({ children }: { children: React.ReactNode }) {
|
||||||
|
return <div className="wrapper">{children}</div>;
|
||||||
|
}
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<span>Child Content</span>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div class="wrapper"><span>Child Content</span></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly render nested HTML elements', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="outer">
|
||||||
|
<p className="inner">
|
||||||
|
<span>Nested content</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div class="outer"><p class="inner"><span>Nested content</span></p></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly render a mix of HTML elements and text', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Text before <span>Element</span> Text after
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Text before <span>Element</span> Text after</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly render text on both sides of an element', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Left side text <strong>Bold text</strong> Right side text
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Left side text <strong>Bold text</strong> Right side text</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly render a mix of curly braces, text, and elements in different orders', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const name = 'World';
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Curly braces, then text, then element */}
|
||||||
|
{name}, Hello <strong>!</strong>
|
||||||
|
{/* Element, then curly braces, then text */}
|
||||||
|
<em>Greetings</em> {name} to you
|
||||||
|
{/* Text, then element, then curly braces */}
|
||||||
|
Welcome <span>dear</span> {name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<div>World, Hello <strong>!</strong><em>Greetings</em> World to you' + 'Welcome <span>dear</span> World</div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('update', () => {
|
||||||
|
it('should support variable assignment of JSX elements and updates', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let element = <div>Hello, World!</div>;
|
||||||
|
|
||||||
|
function updateElement() {
|
||||||
|
element = <div>Updated World!</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{element}
|
||||||
|
<button onClick={updateElement}>Update</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Hello, World!</div><button>Update</button>');
|
||||||
|
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container.innerHTML).toBe('<div>Updated World!</div><button>Update</button>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support JSX elements in arrays with updates', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let elements = [<div>First</div>, <div>Second</div>, <div>Third</div>];
|
||||||
|
|
||||||
|
function updateElements() {
|
||||||
|
elements = [<div>Updated First</div>, <div>Updated Second</div>, <div>Updated Third</div>];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{elements}
|
||||||
|
<button onClick={updateElements}>Update</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>First</div><div>Second</div><div>Third</div><button>Update</button>');
|
||||||
|
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<div>Updated First</div><div>Updated Second</div><div>Updated Third</div><button>Update</button>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support JSX elements as object properties with updates', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let obj = {
|
||||||
|
header: <h1>Title</h1>,
|
||||||
|
content: <p>Content</p>,
|
||||||
|
footer: <footer>Footer</footer>,
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateObj() {
|
||||||
|
obj = {
|
||||||
|
header: <h1>Updated Title</h1>,
|
||||||
|
content: <p>Updated Content</p>,
|
||||||
|
footer: <footer>Updated Footer</footer>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{obj.header}
|
||||||
|
{obj.content}
|
||||||
|
{obj.footer}
|
||||||
|
<button onClick={updateObj}>Update</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<h1>Title</h1><p>Content</p><footer>Footer</footer><button>Update</button>');
|
||||||
|
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<h1>Updated Title</h1><p>Updated Content</p><footer>Updated Footer</footer><button>Update</button>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// it('should support functions returning JSX elements with updates', ({ container }) => {
|
||||||
|
// function App() {
|
||||||
|
// let text = 'Hello';
|
||||||
|
//
|
||||||
|
// const getElement = (t: string) => <span>{t}</span>;
|
||||||
|
//
|
||||||
|
// function updateText() {
|
||||||
|
// text = 'Updated Hello';
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return (
|
||||||
|
// <div>
|
||||||
|
// {getElement(text)}
|
||||||
|
// <button onClick={updateText}>Update</button>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// render(App, container);
|
||||||
|
// expect(container.innerHTML).toBe('<div><span>Hello</span><button>Update</button></div>');
|
||||||
|
//
|
||||||
|
// container.querySelector('button')?.click();
|
||||||
|
// expect(container.innerHTML).toBe('<div><span>Updated Hello</span><button>Update</button></div>');
|
||||||
|
// });
|
||||||
|
|
||||||
|
it('should support JSX elements in conditional expressions with updates', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let condition = true;
|
||||||
|
|
||||||
|
function toggleCondition() {
|
||||||
|
condition = !condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{condition ? <div>True</div> : <div>False</div>}
|
||||||
|
{condition && <div>Conditional</div>}
|
||||||
|
<button onClick={toggleCondition}>Toggle</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>True</div><div>Conditional</div><button>Toggle</button>');
|
||||||
|
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container.innerHTML).toBe('<div>False</div><button>Toggle</button>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('JSX Element Attributes', () => {
|
||||||
|
it('should correctly initialize attributes', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div id="test" className="example">
|
||||||
|
Content
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div id="test" class="example">Content</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly update attributes', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let className = 'initial';
|
||||||
|
|
||||||
|
function updateClass() {
|
||||||
|
className = 'updated';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={className}>Content</div>
|
||||||
|
<button onClick={updateClass}>Update</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div class="initial">Content</div><button>Update</button>');
|
||||||
|
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container.innerHTML).toBe('<div class="updated">Content</div><button>Update</button>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly render attributes dependent on variables', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let className = 'initial';
|
||||||
|
let b = className;
|
||||||
|
function updateClass() {
|
||||||
|
className = 'updated';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={b}>Content</div>
|
||||||
|
<button onClick={updateClass}>Update</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div class="initial">Content</div><button>Update</button>');
|
||||||
|
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container.innerHTML).toBe('<div class="updated">Content</div><button>Update</button>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly render attributes with expressions', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const count = 5;
|
||||||
|
return <div data-count={`Count is ${count}`}>Content</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div data-count="Count is 5">Content</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly render boolean attributes', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const disabled = true;
|
||||||
|
return <button disabled={disabled}>Click me</button>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<button disabled="">Click me</button>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.fails('should correctly render attributes without values', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const checked = true;
|
||||||
|
return <input type="checkbox" checked />;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<input type="checkbox" checked="">');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.fails('should correctly spread multiple attributes', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const props = {
|
||||||
|
id: 'test-id',
|
||||||
|
className: 'test-class',
|
||||||
|
'data-test': 'test-data',
|
||||||
|
};
|
||||||
|
return <div {...props}>Content</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div id="test-id" class="test-class" data-test="test-data">Content</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.fails('should correctly handle attribute spreading and individual props', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const props = {
|
||||||
|
id: 'base-id',
|
||||||
|
className: 'base-class',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div {...props} id="override-id" data-extra="extra">
|
||||||
|
Content
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div id="override-id" class="base-class" data-extra="extra">Content</div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('JSX Element Inline Styles', () => {
|
||||||
|
it('should correctly apply inline styles to an element', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return <div style={{ color: 'red', fontSize: '16px' }}>Styled content</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div style="color: red; font-size: 16px;">Styled content</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly apply multiple inline styles to an element', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
return <div style={{ color: 'blue', fontSize: '20px', fontWeight: 'bold', margin: '10px' }}>Multiple styles</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<div style="color: blue; font-size: 20px; font-weight: bold; margin: 10px;">Multiple styles</div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly apply styles from a variable', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const styleObj = { color: 'green', padding: '5px' };
|
||||||
|
return <div style={styleObj}>Variable style</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div style="color: green; padding: 5px;">Variable style</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly update styles', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let style = { color: 'purple' };
|
||||||
|
|
||||||
|
function updateStyle() {
|
||||||
|
style = { color: 'orange', fontSize: '24px' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={style}>Updatable style</div>
|
||||||
|
<button onClick={updateStyle}>Update</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div style="color: purple;">Updatable style</div><button>Update</button>');
|
||||||
|
|
||||||
|
container.querySelector('button')?.click();
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<div style="color: orange; font-size: 24px;">Updatable style</div><button>Update</button>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly apply styles from an expression', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const size = 18;
|
||||||
|
return <div style={{ fontSize: `${size}px`, lineHeight: `${size * 1.5}px` }}>Expression style</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div style="font-size: 18px; line-height: 27px;">Expression style</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly merge style objects', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const baseStyle = { color: 'red', fontSize: '16px' };
|
||||||
|
const additionalStyle = { fontSize: '20px', fontWeight: 'bold' };
|
||||||
|
return <div style={{ ...baseStyle, ...additionalStyle }}>Merged styles</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<div style="color: red; font-size: 20px; font-weight: bold;">Merged styles</div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly apply styles from a function call', ({ container }) => {
|
||||||
|
function getStyles(color: string) {
|
||||||
|
return { color, border: `1px solid ${color}` };
|
||||||
|
}
|
||||||
|
function App() {
|
||||||
|
return <div style={getStyles('blue')}>Function style</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div style="color: blue; border: 1px solid blue;">Function style</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly apply styles based on a condition', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const isActive = true;
|
||||||
|
const style = isActive
|
||||||
|
? { backgroundColor: 'green', color: 'white' }
|
||||||
|
: { backgroundColor: 'gray', color: 'black' };
|
||||||
|
return <div style={style}>Conditional style</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div style="background-color: green; color: white;">Conditional style</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly apply styles based on an array of conditions', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const conditions = [true, false, true];
|
||||||
|
const style = {
|
||||||
|
color: conditions[0] ? 'red' : 'blue',
|
||||||
|
fontWeight: conditions[1] ? 'bold' : 'normal',
|
||||||
|
fontSize: conditions[2] ? '20px' : '16px',
|
||||||
|
};
|
||||||
|
return <div style={style}>Array condition style</div>;
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<div style="color: red; font-weight: normal; font-size: 20px;">Array condition style</div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly apply styles using ternary and binary expressions', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
const isPrimary = true;
|
||||||
|
const isLarge = true;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: isPrimary ? 'blue' : 'gray',
|
||||||
|
fontSize: isLarge && '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ternary and binary style
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div style="color: blue; font-size: 24px;">Ternary and binary style</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly apply styles using member expressions', ({ container }) => {
|
||||||
|
const theme = {
|
||||||
|
colors: {
|
||||||
|
primary: 'blue',
|
||||||
|
secondary: 'green',
|
||||||
|
},
|
||||||
|
sizes: {
|
||||||
|
small: '12px',
|
||||||
|
medium: '16px',
|
||||||
|
large: '20px',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: theme.colors.primary,
|
||||||
|
fontSize: theme.sizes.medium,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Member expression style
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div style="color: blue; font-size: 16px;">Member expression style</div>');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,69 +1,184 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
*
|
*
|
||||||
* openInula is licensed under Mulan PSL v2.
|
* openInula is licensed under Mulan PSL v2.
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
*
|
*
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
*
|
*
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, vi } from 'vitest';
|
import { describe, expect, vi, beforeEach } from 'vitest';
|
||||||
import { domTest as it } from './utils';
|
import { domTest as it } from './utils';
|
||||||
import { render, View } from '../src';
|
import { render, didMount, willMount, didUnmount, willUnmount } from '../src';
|
||||||
|
vi.mock('../src/scheduler', async () => {
|
||||||
describe('lifecycle', () => {
|
return {
|
||||||
it('should call willMount', ({ container }) => {
|
schedule: (task: () => void) => {
|
||||||
const fn = vi.fn();
|
task();
|
||||||
|
},
|
||||||
function App() {
|
};
|
||||||
willMount: {
|
});
|
||||||
expect(container.innerHTML).toBe('');
|
|
||||||
fn();
|
describe('lifecycle', () => {
|
||||||
}
|
it('should call willMount', ({ container }) => {
|
||||||
|
const fn = vi.fn();
|
||||||
return <div>test</div>;
|
|
||||||
}
|
function App() {
|
||||||
|
willMount(() => {
|
||||||
render(App, container);
|
expect(container.innerHTML).toBe('');
|
||||||
expect(fn).toHaveBeenCalled();
|
fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call didMount', ({ container }) => {
|
return <div>test</div>;
|
||||||
const fn = vi.fn();
|
}
|
||||||
|
|
||||||
function App() {
|
render(App, container);
|
||||||
didMount: {
|
expect(fn).toHaveBeenCalled();
|
||||||
expect(container.innerHTML).toBe('<div>test</div>');
|
});
|
||||||
fn();
|
|
||||||
}
|
it('should call didMount', ({ container }) => {
|
||||||
|
const fn = vi.fn();
|
||||||
return <div>test</div>;
|
|
||||||
}
|
function App() {
|
||||||
|
didMount(() => {
|
||||||
render(App, container);
|
expect(container.innerHTML).toBe('<div>test</div>');
|
||||||
expect(fn).toHaveBeenCalled();
|
fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: implement unmount
|
return <div>test</div>;
|
||||||
it.skip('should call willUnmount', ({ container }) => {
|
}
|
||||||
const fn = vi.fn();
|
|
||||||
|
render(App, container);
|
||||||
function App() {
|
expect(fn).toHaveBeenCalled();
|
||||||
willUnmount: {
|
});
|
||||||
expect(container.innerHTML).toBe('<div>test</div>');
|
|
||||||
fn();
|
it('should handle async operations in didMount', async ({ container }) => {
|
||||||
}
|
function App() {
|
||||||
|
let users: string[] = [];
|
||||||
return <div>test</div>;
|
|
||||||
}
|
didMount(async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
render(App, container);
|
users = ['Alice', 'Bob'];
|
||||||
expect(fn).toHaveBeenCalled();
|
});
|
||||||
});
|
|
||||||
});
|
return <div>{users.join(', ')}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.useFakeTimers();
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div></div>');
|
||||||
|
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
|
expect(container.innerHTML).toBe('<div>Alice, Bob</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle async errors in didMount with try-catch', async ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let text = 'initial';
|
||||||
|
|
||||||
|
didMount(async () => {
|
||||||
|
try {
|
||||||
|
await new Promise((_, reject) => setTimeout(() => reject(new Error('Async error')), 0));
|
||||||
|
} catch (error) {
|
||||||
|
text = 'Error caught';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div>{text}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.useFakeTimers();
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>initial</div>');
|
||||||
|
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
|
expect(container.innerHTML).toBe('<div>Error caught</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('willUnmount', () => {
|
||||||
|
// TODO: implement unmount
|
||||||
|
it.fails('should call willUnmount', ({ container }) => {
|
||||||
|
const fn = vi.fn();
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
didUnmount(() => {
|
||||||
|
expect(container.innerHTML).toBe('<div>test</div>');
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div>test</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(fn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const fn = vi.fn();
|
||||||
|
|
||||||
|
function Child() {
|
||||||
|
willUnmount(() => {
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div>test</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fn.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call willUnmount in if condition changed', ({ container }) => {
|
||||||
|
let setCond: (cond: boolean) => void;
|
||||||
|
function App() {
|
||||||
|
let cond = true;
|
||||||
|
setCond = (value: boolean) => {
|
||||||
|
cond = value;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<if cond={cond}>
|
||||||
|
<Child />
|
||||||
|
</if>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
setCond!(false);
|
||||||
|
expect(fn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call willUnmount in expression updated', ({ container }) => {
|
||||||
|
let setCond: (cond: boolean) => void;
|
||||||
|
function App() {
|
||||||
|
let cond = true;
|
||||||
|
setCond = (value: boolean) => {
|
||||||
|
cond = value;
|
||||||
|
};
|
||||||
|
return <div>{cond ? <Child /> : null}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
setCond!(false);
|
||||||
|
expect(fn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call willUnmount in for', ({ container }) => {
|
||||||
|
let setArr: (arr: string[]) => void;
|
||||||
|
function App() {
|
||||||
|
let arr = ['a', 'b', 'c'];
|
||||||
|
setArr = (value: string[]) => {
|
||||||
|
arr = value;
|
||||||
|
};
|
||||||
|
return <for each={arr}>{item => <Child />}</for>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
setArr!([]);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,139 +1,328 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
*
|
*
|
||||||
* openInula is licensed under Mulan PSL v2.
|
* openInula is licensed under Mulan PSL v2.
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
*
|
*
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
*
|
*
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, vi } from 'vitest';
|
import { describe, expect, vi } from 'vitest';
|
||||||
import { domTest as it } from './utils';
|
import { domTest as it } from './utils';
|
||||||
import { render, View } from '../src';
|
import { render, View } from '../src';
|
||||||
|
|
||||||
describe('props', () => {
|
describe('props', () => {
|
||||||
describe('normal props', () => {
|
describe('normal props', () => {
|
||||||
it('should support prop', ({ container }) => {
|
it('should support prop', ({ container }) => {
|
||||||
function Child({ name }) {
|
function Child({ name }) {
|
||||||
return <h1>{name}</h1>;
|
return <h1>{name}</h1>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <Child name={'hello world!!!'} />;
|
return <Child name={'hello world!!!'} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(App, container);
|
render(App, container);
|
||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<h1>
|
<h1>
|
||||||
hello world!!!
|
hello world!!!
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support prop alias', ({ container }) => {
|
it('should support prop alias', ({ container }) => {
|
||||||
function Child({ name: alias }) {
|
function Child({ name: alias }) {
|
||||||
return <h1>{alias}</h1>;
|
return <h1>{alias}</h1>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <Child name={'prop alias'} />;
|
return <Child name={'prop alias'} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(App, container);
|
render(App, container);
|
||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<h1>
|
<h1>
|
||||||
prop alias
|
prop alias
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support prop alias with default value', ({ container }) => {
|
it('should support prop alias with default value', ({ container }) => {
|
||||||
function Child({ name: alias = 'default' }) {
|
function Child({ name: alias = 'default' }) {
|
||||||
return <h1>{alias}</h1>;
|
return <h1>{alias}</h1>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <Child />;
|
return <Child />;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(App, container);
|
render(App, container);
|
||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<h1>
|
<h1>
|
||||||
default
|
default
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('children', () => {
|
describe('children', () => {
|
||||||
it('should support children', ({ container }) => {
|
it('should support children', ({ container }) => {
|
||||||
function Child({ children }) {
|
function Child({ children }) {
|
||||||
return <h1>{children}</h1>;
|
return <h1>{children}</h1>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <Child>child content</Child>;
|
return <Child>child content</Child>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(App, container);
|
render(App, container);
|
||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<h1>
|
<h1>
|
||||||
child content
|
child content
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support children alias', ({ container }) => {
|
it('should support children alias', ({ container }) => {
|
||||||
function Child({ children: alias }) {
|
function Child({ children: alias }) {
|
||||||
return <h1>{alias}</h1>;
|
return <h1>{alias}</h1>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <Child>children alias</Child>;
|
return <Child>children alias</Child>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(App, container);
|
render(App, container);
|
||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<h1>
|
<h1>
|
||||||
children alias
|
children alias
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: should support children default
|
it('should support children alias with default value', ({ container }) => {
|
||||||
it.fails('should support children alias with default value', ({ container }) => {
|
function Child({ children: alias = 'default child' }) {
|
||||||
function Child({ children: alias = 'default child' }) {
|
return <h1>{alias}</h1>;
|
||||||
return <h1>{alias}</h1>;
|
}
|
||||||
}
|
|
||||||
|
function App() {
|
||||||
function App() {
|
return <Child />;
|
||||||
return <Child />;
|
}
|
||||||
}
|
|
||||||
|
render(App, container);
|
||||||
render(App, container);
|
expect(container).toMatchInlineSnapshot(`
|
||||||
expect(container).toMatchInlineSnapshot(`
|
<div>
|
||||||
<div>
|
<h1>
|
||||||
<h1>
|
default child
|
||||||
default child
|
</h1>
|
||||||
</h1>
|
</div>
|
||||||
</div>
|
`);
|
||||||
`);
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
describe('extended prop tests', () => {
|
||||||
|
it('should correctly pass and render string props', ({ container }) => {
|
||||||
|
function Child({ text }) {
|
||||||
|
return <p>{text}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <Child text="Hello, world!" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Hello, world!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly pass and render number props', ({ container }) => {
|
||||||
|
function Child({ number }) {
|
||||||
|
return <span>{number}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <Child number={42} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
42
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly pass and render boolean props', ({ container }) => {
|
||||||
|
function Child({ isActive }) {
|
||||||
|
return <div>{isActive ? 'Active' : 'Inactive'}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <Child isActive={true} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Active
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.fails('should correctly pass and render array props', ({ container }) => {
|
||||||
|
function Child({ items }) {
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<li key={index}>{item}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <Child items={['Apple', 'Banana', 'Cherry']} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li>Apple</li>
|
||||||
|
<li>Banana</li>
|
||||||
|
<li>Cherry</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.fails('should correctly pass and render object props', ({ container }) => {
|
||||||
|
function Child({ person }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{person.name}, {person.age}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <Child person={{ name: 'Alice', age: 30 }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Alice, 30
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly handle default prop values', ({ container }) => {
|
||||||
|
function Child({ message = 'Default message' }) {
|
||||||
|
return <h2>{message}</h2>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <Child />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<h2>
|
||||||
|
Default message
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.fails('should correctly spread props', ({ container }) => {
|
||||||
|
function Child(props) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{props.a} {props.b} {props.c}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const extraProps = { b: 'World', c: '!' };
|
||||||
|
return <Child a="Hello" {...extraProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Hello World !
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.fails('should handle props without values', ({ container }) => {
|
||||||
|
function Child({ isDisabled }) {
|
||||||
|
return <button disabled={isDisabled}>Click me</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <Child isDisabled />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<button disabled>
|
||||||
|
Click me
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle props with expressions', ({ container }) => {
|
||||||
|
function Child({ result }) {
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <Child result={1 + 2 * 3} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
7
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,203 +1,322 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
*
|
*
|
||||||
* openInula is licensed under Mulan PSL v2.
|
* openInula is licensed under Mulan PSL v2.
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
*
|
*
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
*
|
*
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, vi } from 'vitest';
|
import { describe, expect, vi } from 'vitest';
|
||||||
import { domTest as it } from './utils';
|
import { domTest as it } from './utils';
|
||||||
import { render, View } from '../src';
|
import { render, View } from '../src';
|
||||||
|
|
||||||
describe('rendering', () => {
|
vi.mock('../src/scheduler', async () => {
|
||||||
describe('basic', () => {
|
return {
|
||||||
it('should support basic dom', ({ container }) => {
|
schedule: (task: () => void) => {
|
||||||
function App() {
|
task();
|
||||||
return <h1>hello world!!!</h1>;
|
},
|
||||||
}
|
};
|
||||||
|
});
|
||||||
render(App, container);
|
|
||||||
expect(container).toMatchInlineSnapshot(`
|
describe('rendering', () => {
|
||||||
<div>
|
describe('basic', () => {
|
||||||
<h1>
|
it('should support basic dom', ({ container }) => {
|
||||||
hello world!!!
|
function App() {
|
||||||
</h1>
|
return <h1>hello world!!!</h1>;
|
||||||
</div>
|
}
|
||||||
`);
|
|
||||||
});
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
it('should support text and variable mixing', ({ container }) => {
|
<div>
|
||||||
function App() {
|
<h1>
|
||||||
const name = 'world';
|
hello world!!!
|
||||||
return <h1>hello {name}!!!</h1>;
|
</h1>
|
||||||
}
|
</div>
|
||||||
|
`);
|
||||||
render(App, container);
|
});
|
||||||
expect(container).toMatchInlineSnapshot(`
|
|
||||||
<div>
|
it('should support text and variable mixing', ({ container }) => {
|
||||||
<h1>
|
function App() {
|
||||||
hello
|
const name = 'world';
|
||||||
world
|
return <h1>hello {name}!!!</h1>;
|
||||||
!!!
|
}
|
||||||
</h1>
|
|
||||||
</div>
|
render(App, container);
|
||||||
`);
|
expect(container.innerHTML).toBe('<h1>hello world!!!</h1>');
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: SHOULD FIX
|
// TODO: SHOULD FIX
|
||||||
it.fails('should support dom has multiple layers ', ({ container }) => {
|
it('should support dom has multiple layers ', ({ container }) => {
|
||||||
function App() {
|
function App() {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
return (
|
||||||
return (
|
<div>
|
||||||
<div>
|
Header
|
||||||
Header
|
<h1>hello world!!!</h1>
|
||||||
<h1>hello world!!!</h1>
|
<section>
|
||||||
<section>
|
<button>Add, Now is{count}</button>
|
||||||
<button>Add, Now is {count}</button>
|
</section>
|
||||||
</section>
|
Footer
|
||||||
Footer
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
render(App, container);
|
||||||
render(App, container);
|
expect(container).toMatchInlineSnapshot(`
|
||||||
expect(container).toMatchInlineSnapshot(`
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
Header
|
||||||
<h1>
|
<h1>
|
||||||
hello world!!!
|
hello world!!!
|
||||||
</h1>
|
</h1>
|
||||||
<section>
|
<section>
|
||||||
<button>
|
<button>
|
||||||
Add, Now is
|
Add, Now is
|
||||||
0
|
0
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
Footer
|
||||||
</div>
|
</div>
|
||||||
`);
|
</div>
|
||||||
});
|
`);
|
||||||
|
});
|
||||||
// TODO: SHOULD FIX
|
|
||||||
it.fails('should support tag, text and variable mixing', ({ container }) => {
|
// TODO: SHOULD FIX
|
||||||
function App() {
|
it('should support tag, text and variable mixing', ({ container }) => {
|
||||||
let count = 'world';
|
function App() {
|
||||||
|
let count = 'world';
|
||||||
return (
|
|
||||||
<section>
|
return (
|
||||||
count: {count}
|
<section>
|
||||||
<button>Add, count is {count}</button>
|
count:{count}
|
||||||
</section>
|
<button>Add, count is{count}</button>
|
||||||
);
|
</section>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
render(App, container);
|
|
||||||
expect(container).toMatchInlineSnapshot();
|
render(App, container);
|
||||||
});
|
expect(container).toMatchInlineSnapshot(`
|
||||||
});
|
<div>
|
||||||
|
<section>
|
||||||
describe('style', () => {
|
count:
|
||||||
it('should apply styles correctly', ({ container }) => {
|
world
|
||||||
function App() {
|
<button>
|
||||||
return <h1 style={{ color: 'red' }}>hello world!!!</h1>;
|
Add, count is
|
||||||
}
|
world
|
||||||
|
</button>
|
||||||
render(App, container);
|
</section>
|
||||||
const h1 = container.querySelector('h1');
|
</div>
|
||||||
expect(h1.style.color).toBe('red');
|
`);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
it('should apply multiple styles correctly', ({ container }) => {
|
|
||||||
function App() {
|
describe('style', () => {
|
||||||
return <h1 style={{ color: 'red', fontSize: '20px' }}>hello world!!!</h1>;
|
it('should apply styles correctly', ({ container }) => {
|
||||||
}
|
function App() {
|
||||||
|
return <h1 style={{ color: 'red' }}>hello world!!!</h1>;
|
||||||
render(App, container);
|
}
|
||||||
const h1 = container.querySelector('h1');
|
|
||||||
expect(h1.style.color).toBe('red');
|
render(App, container);
|
||||||
expect(h1.style.fontSize).toBe('20px');
|
const h1 = container.querySelector('h1');
|
||||||
});
|
expect(h1.style.color).toBe('red');
|
||||||
|
});
|
||||||
it('should override styles correctly', ({ container }) => {
|
|
||||||
function App() {
|
it.fails('should apply styles number correctly', ({ container }) => {
|
||||||
return (
|
function App() {
|
||||||
<h1 style={{ color: 'red' }}>
|
return <h1 style={{ fontSize: 12 }}>hello world!!!</h1>;
|
||||||
<span style={{ color: 'blue' }}>hello world!!!</span>
|
}
|
||||||
</h1>
|
|
||||||
);
|
render(App, container);
|
||||||
}
|
const h1 = container.querySelector('h1');
|
||||||
|
expect(h1.style.fontSize).toBe('12px');
|
||||||
render(App, container);
|
});
|
||||||
const span = container.querySelector('span');
|
|
||||||
expect(span.style.color).toBe('blue');
|
it('should apply styles empty correctly', ({ container }) => {
|
||||||
});
|
function App() {
|
||||||
|
return <h1 style={{ fontSize: '' }}>hello world!!!</h1>;
|
||||||
it('should handle dynamic styles', ({ container }) => {
|
}
|
||||||
const color = 'red';
|
|
||||||
|
render(App, container);
|
||||||
function App() {
|
const h1 = container.querySelector('h1');
|
||||||
return <h1 style={{ color }}>hello world!!!</h1>;
|
expect(h1.style.fontSize).toBe('');
|
||||||
}
|
});
|
||||||
|
|
||||||
render(App, container);
|
it('should apply multiple styles correctly', ({ container }) => {
|
||||||
const h1 = container.querySelector('h1');
|
function App() {
|
||||||
expect(h1.style.color).toBe('red');
|
return <h1 style={{ color: 'red', fontSize: '20px' }}>hello world!!!</h1>;
|
||||||
});
|
}
|
||||||
});
|
|
||||||
|
render(App, container);
|
||||||
describe('event', () => {
|
const h1 = container.querySelector('h1');
|
||||||
it('should handle click events', ({ container }) => {
|
expect(h1.style.color).toBe('red');
|
||||||
const handleClick = vi.fn();
|
expect(h1.style.fontSize).toBe('20px');
|
||||||
|
});
|
||||||
function App() {
|
|
||||||
return <button onClick={handleClick}>Click me</button>;
|
it('should override styles correctly', ({ container }) => {
|
||||||
}
|
function App() {
|
||||||
|
return (
|
||||||
render(App, container);
|
<h1 style={{ color: 'red' }}>
|
||||||
const button = container.querySelector('button');
|
<span style={{ color: 'blue' }}>hello world!!!</span>
|
||||||
button.click();
|
</h1>
|
||||||
|
);
|
||||||
expect(handleClick).toHaveBeenCalled();
|
}
|
||||||
});
|
|
||||||
});
|
render(App, container);
|
||||||
|
const span = container.querySelector('span');
|
||||||
describe('components', () => {
|
expect(span.style.color).toBe('blue');
|
||||||
it('should render components', ({ container }) => {
|
});
|
||||||
function Button({ children }) {
|
|
||||||
return <button>{children}</button>;
|
it('should handle dynamic styles', ({ container }) => {
|
||||||
}
|
const color = 'red';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return <h1 style={{ color }}>hello world!!!</h1>;
|
||||||
<div>
|
}
|
||||||
<h1>hello world!!!</h1>
|
|
||||||
<Button>Click me</Button>
|
render(App, container);
|
||||||
</div>
|
const h1 = container.querySelector('h1');
|
||||||
);
|
expect(h1.style.color).toBe('red');
|
||||||
}
|
});
|
||||||
|
|
||||||
render(App, container);
|
it('should handle style object', ({ container }) => {
|
||||||
expect(container).toMatchInlineSnapshot(`
|
const color = 'red';
|
||||||
<div>
|
|
||||||
<div>
|
function App() {
|
||||||
<h1>
|
const style = { color };
|
||||||
hello world!!!
|
return <h1 style={style}>hello world!!!</h1>;
|
||||||
</h1>
|
}
|
||||||
<button>
|
|
||||||
Click me
|
render(App, container);
|
||||||
</button>
|
const h1 = container.querySelector('h1');
|
||||||
</div>
|
expect(h1.style.color).toBe('red');
|
||||||
</div>
|
});
|
||||||
`);
|
|
||||||
});
|
it('should update style', ({ container }) => {
|
||||||
});
|
function App() {
|
||||||
});
|
let color = 'red';
|
||||||
|
return (
|
||||||
|
<h1 style={{ color }} onClick={() => (color = 'blue')}>
|
||||||
|
hello world!!!
|
||||||
|
</h1>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
const h1 = container.querySelector('h1');
|
||||||
|
expect(h1.style.color).toBe('red');
|
||||||
|
h1.click();
|
||||||
|
expect(h1.style.color).toBe('blue');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update when object style changed', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let style = { color: 'red', fontSize: '20px' };
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={style}
|
||||||
|
onClick={() => {
|
||||||
|
style = { color: 'blue', height: '20px' };
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
hello world!!!
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
const div = container.querySelector('div');
|
||||||
|
expect(div.style.color).toBe('red');
|
||||||
|
expect(div.style.fontSize).toBe('20px');
|
||||||
|
expect(div.style.height).toBe('');
|
||||||
|
div.click();
|
||||||
|
expect(div.style.color).toBe('blue');
|
||||||
|
expect(div.style.height).toBe('20px');
|
||||||
|
expect(div.style.fontSize).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('event', () => {
|
||||||
|
it('should handle click events', ({ container }) => {
|
||||||
|
const handleClick = vi.fn();
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <button onClick={handleClick}>Click me</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
const button = container.querySelector('button');
|
||||||
|
button.click();
|
||||||
|
|
||||||
|
expect(handleClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support inline event update', ({ container }) => {
|
||||||
|
function App() {
|
||||||
|
let count = 0;
|
||||||
|
return <button onClick={() => (count += 1)}>add:{count}</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toEqual('<button>add:0</button>');
|
||||||
|
const button = container.querySelector('button');
|
||||||
|
button.click();
|
||||||
|
expect(container.innerHTML).toEqual('<button>add:1</button>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support inline event for component props', ({ container }) => {
|
||||||
|
function Button({ onClick, children }) {
|
||||||
|
return <button onClick={onClick}>{children}</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let value = '';
|
||||||
|
|
||||||
|
return <Button onClick={() => (value = 'World')}>Hello {value}!</Button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container.innerHTML).toEqual('<button>Hello !</button>');
|
||||||
|
const button = container.querySelector('button')!;
|
||||||
|
button.click();
|
||||||
|
expect(container.innerHTML).toEqual('<button>Hello World!</button>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
it('should render components', ({ container }) => {
|
||||||
|
function Button({ children }) {
|
||||||
|
return <button>{children}</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>hello world!!!</h1>
|
||||||
|
<Button>Click me</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(App, container);
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1>
|
||||||
|
hello world!!!
|
||||||
|
</h1>
|
||||||
|
<button>
|
||||||
|
Click me
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,375 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, vi } from 'vitest';
|
||||||
|
import { domTest as it } from './utils';
|
||||||
|
import { render, View } from '../src';
|
||||||
|
|
||||||
|
vi.mock('../src/scheduler', async () => {
|
||||||
|
return {
|
||||||
|
schedule: (task: () => void) => {
|
||||||
|
task();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Declare state', () => {
|
||||||
|
it('Should correctly declare and render a string state variable', ({ container }) => {
|
||||||
|
function StringState() {
|
||||||
|
let str = 'Hello, World!';
|
||||||
|
return <p>{str}</p>;
|
||||||
|
}
|
||||||
|
render(StringState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>Hello, World!</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly declare and render a number state variable', ({ container }) => {
|
||||||
|
function NumberState() {
|
||||||
|
let num = 42;
|
||||||
|
return <p>{num}</p>;
|
||||||
|
}
|
||||||
|
render(NumberState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>42</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly declare and render a boolean state variable', ({ container }) => {
|
||||||
|
function BooleanState() {
|
||||||
|
let bool = true;
|
||||||
|
return <p>{bool.toString()}</p>;
|
||||||
|
}
|
||||||
|
render(BooleanState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>true</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly declare and render an array state variable', ({ container }) => {
|
||||||
|
function ArrayState() {
|
||||||
|
let arr = [1, 2, 3];
|
||||||
|
return <p>{arr.join(', ')}</p>;
|
||||||
|
}
|
||||||
|
render(ArrayState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>1, 2, 3</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly declare and render an object state variable', ({ container }) => {
|
||||||
|
function ObjectState() {
|
||||||
|
let obj = { name: 'John', age: 30 };
|
||||||
|
return <p>{`${obj.name}, ${obj.age}`}</p>;
|
||||||
|
}
|
||||||
|
render(ObjectState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>John, 30</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly declare and render a map state variable', ({ container }) => {
|
||||||
|
function MapState() {
|
||||||
|
let map = new Map([
|
||||||
|
['key1', 'value1'],
|
||||||
|
['key2', 'value2'],
|
||||||
|
]);
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
{Array.from(map)
|
||||||
|
.map(([k, v]) => `${k}:${v}`)
|
||||||
|
.join(', ')}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(MapState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>key1:value1, key2:value2</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly declare and render a set state variable', ({ container }) => {
|
||||||
|
function SetState() {
|
||||||
|
let set = new Set([1, 2, 3]);
|
||||||
|
return <p>{Array.from(set).join(', ')}</p>;
|
||||||
|
}
|
||||||
|
render(SetState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>1, 2, 3</p>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Update state', () => {
|
||||||
|
it('Should correctly update and render a string state variable', ({ container }) => {
|
||||||
|
let updateStr: () => void;
|
||||||
|
function StringState() {
|
||||||
|
let str = 'Hello';
|
||||||
|
updateStr = () => {
|
||||||
|
str = 'Hello, World!';
|
||||||
|
};
|
||||||
|
return <p>{str}</p>;
|
||||||
|
}
|
||||||
|
render(StringState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>Hello</p>');
|
||||||
|
updateStr();
|
||||||
|
expect(container.innerHTML).toBe('<p>Hello, World!</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly update and render a number state variable', ({ container }) => {
|
||||||
|
let updateNum: () => void;
|
||||||
|
function NumberState() {
|
||||||
|
let num = 42;
|
||||||
|
updateNum = () => {
|
||||||
|
num = 84;
|
||||||
|
};
|
||||||
|
return <p>{num}</p>;
|
||||||
|
}
|
||||||
|
render(NumberState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>42</p>');
|
||||||
|
updateNum();
|
||||||
|
expect(container.innerHTML).toBe('<p>84</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly update and render a boolean state variable', ({ container }) => {
|
||||||
|
let toggleBool: () => void;
|
||||||
|
function BooleanState() {
|
||||||
|
let bool = true;
|
||||||
|
toggleBool = () => {
|
||||||
|
bool = !bool;
|
||||||
|
};
|
||||||
|
return <p>{bool.toString()}</p>;
|
||||||
|
}
|
||||||
|
render(BooleanState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>true</p>');
|
||||||
|
toggleBool();
|
||||||
|
expect(container.innerHTML).toBe('<p>false</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly update and render an object state variable', ({ container }) => {
|
||||||
|
let updateObj: () => void;
|
||||||
|
function ObjectState() {
|
||||||
|
let obj = { name: 'John', age: 30 };
|
||||||
|
updateObj = () => {
|
||||||
|
obj.age = 31;
|
||||||
|
};
|
||||||
|
return <p>{`${obj.name}, ${obj.age}`}</p>;
|
||||||
|
}
|
||||||
|
render(ObjectState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>John, 30</p>');
|
||||||
|
updateObj();
|
||||||
|
expect(container.innerHTML).toBe('<p>John, 31</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly handle increment operations (n++)', ({ container }) => {
|
||||||
|
let increment: () => void;
|
||||||
|
function IncrementState() {
|
||||||
|
let count = 0;
|
||||||
|
increment = () => {
|
||||||
|
count++;
|
||||||
|
};
|
||||||
|
return <p>{count}</p>;
|
||||||
|
}
|
||||||
|
render(IncrementState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>0</p>');
|
||||||
|
increment();
|
||||||
|
expect(container.innerHTML).toBe('<p>1</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly handle decrement operations (n--)', ({ container }) => {
|
||||||
|
let decrement: () => void;
|
||||||
|
function DecrementState() {
|
||||||
|
let count = 5;
|
||||||
|
decrement = () => {
|
||||||
|
count--;
|
||||||
|
};
|
||||||
|
return <p>{count}</p>;
|
||||||
|
}
|
||||||
|
render(DecrementState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>5</p>');
|
||||||
|
decrement();
|
||||||
|
expect(container.innerHTML).toBe('<p>4</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly handle operations (+=)', ({ container }) => {
|
||||||
|
let addValue: (value: number) => void;
|
||||||
|
function AdditionAssignmentState() {
|
||||||
|
let count = 10;
|
||||||
|
addValue = (value: number) => {
|
||||||
|
count += value;
|
||||||
|
};
|
||||||
|
return <p>{count}</p>;
|
||||||
|
}
|
||||||
|
render(AdditionAssignmentState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>10</p>');
|
||||||
|
addValue(5);
|
||||||
|
expect(container.innerHTML).toBe('<p>15</p>');
|
||||||
|
addValue(-3);
|
||||||
|
expect(container.innerHTML).toBe('<p>12</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly handle operations (-=)', ({ container }) => {
|
||||||
|
let subtractValue: (value: number) => void;
|
||||||
|
function SubtractionAssignmentState() {
|
||||||
|
let count = 20;
|
||||||
|
subtractValue = (value: number) => {
|
||||||
|
count -= value;
|
||||||
|
};
|
||||||
|
return <p>{count}</p>;
|
||||||
|
}
|
||||||
|
render(SubtractionAssignmentState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>20</p>');
|
||||||
|
subtractValue(7);
|
||||||
|
expect(container.innerHTML).toBe('<p>13</p>');
|
||||||
|
subtractValue(-4);
|
||||||
|
expect(container.innerHTML).toBe('<p>17</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly update and render a state variable as an index of array', ({ container }) => {
|
||||||
|
let updateIndex: () => void;
|
||||||
|
function ArrayIndexState() {
|
||||||
|
const items = ['Apple', 'Banana', 'Cherry', 'Date'];
|
||||||
|
let index = 0;
|
||||||
|
updateIndex = () => {
|
||||||
|
index = (index + 1) % items.length;
|
||||||
|
};
|
||||||
|
return <p>{items[index]}</p>;
|
||||||
|
}
|
||||||
|
render(ArrayIndexState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>Apple</p>');
|
||||||
|
updateIndex();
|
||||||
|
expect(container.innerHTML).toBe('<p>Banana</p>');
|
||||||
|
updateIndex();
|
||||||
|
expect(container.innerHTML).toBe('<p>Cherry</p>');
|
||||||
|
updateIndex();
|
||||||
|
expect(container.innerHTML).toBe('<p>Date</p>');
|
||||||
|
updateIndex();
|
||||||
|
expect(container.innerHTML).toBe('<p>Apple</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly update and render a state variable as a property of an object', ({ container }) => {
|
||||||
|
let updatePerson: () => void;
|
||||||
|
function ObjectPropertyState() {
|
||||||
|
let person = { name: 'Alice', age: 30, job: 'Engineer' };
|
||||||
|
updatePerson = () => {
|
||||||
|
person.age += 1;
|
||||||
|
person.job = 'Senior Engineer';
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Name: {person.name}</p>
|
||||||
|
<p>Age: {person.age}</p>
|
||||||
|
<p>Job: {person.job}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(ObjectPropertyState, container);
|
||||||
|
expect(container.innerHTML).toBe('<div><p>Name: Alice</p><p>Age: 30</p><p>Job: Engineer</p></div>');
|
||||||
|
updatePerson();
|
||||||
|
expect(container.innerHTML).toBe('<div><p>Name: Alice</p><p>Age: 31</p><p>Job: Senior Engineer</p></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly update and render an array state variable - push operation', ({ container }) => {
|
||||||
|
let pushItem: () => void;
|
||||||
|
function ArrayPushState() {
|
||||||
|
let items = ['Apple', 'Banana'];
|
||||||
|
pushItem = () => {
|
||||||
|
items.push('Cherry');
|
||||||
|
};
|
||||||
|
return <p>{items.join(', ')}</p>;
|
||||||
|
}
|
||||||
|
render(ArrayPushState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>Apple, Banana</p>');
|
||||||
|
pushItem();
|
||||||
|
expect(container.innerHTML).toBe('<p>Apple, Banana, Cherry</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly update and render an array state variable - pop operation', ({ container }) => {
|
||||||
|
let popItem: () => void;
|
||||||
|
function ArrayPopState() {
|
||||||
|
let items = ['Apple', 'Banana', 'Cherry'];
|
||||||
|
popItem = () => {
|
||||||
|
items.pop();
|
||||||
|
};
|
||||||
|
return <p>{items.join(', ')}</p>;
|
||||||
|
}
|
||||||
|
render(ArrayPopState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>Apple, Banana, Cherry</p>');
|
||||||
|
popItem();
|
||||||
|
expect(container.innerHTML).toBe('<p>Apple, Banana</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly update and render an array state variable - unshift operation', ({ container }) => {
|
||||||
|
let unshiftItem: () => void;
|
||||||
|
function ArrayUnshiftState() {
|
||||||
|
let items = ['Banana', 'Cherry'];
|
||||||
|
unshiftItem = () => {
|
||||||
|
items.unshift('Apple');
|
||||||
|
};
|
||||||
|
return <p>{items.join(', ')}</p>;
|
||||||
|
}
|
||||||
|
render(ArrayUnshiftState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>Banana, Cherry</p>');
|
||||||
|
unshiftItem();
|
||||||
|
expect(container.innerHTML).toBe('<p>Apple, Banana, Cherry</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly update and render an array state variable - shift operation', ({ container }) => {
|
||||||
|
let shiftItem: () => void;
|
||||||
|
function ArrayShiftState() {
|
||||||
|
let items = ['Apple', 'Banana', 'Cherry'];
|
||||||
|
shiftItem = () => {
|
||||||
|
items.shift();
|
||||||
|
};
|
||||||
|
return <p>{items.join(', ')}</p>;
|
||||||
|
}
|
||||||
|
render(ArrayShiftState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>Apple, Banana, Cherry</p>');
|
||||||
|
shiftItem();
|
||||||
|
expect(container.innerHTML).toBe('<p>Banana, Cherry</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly update and render an array state variable - splice operation', ({ container }) => {
|
||||||
|
let spliceItems: () => void;
|
||||||
|
function ArraySpliceState() {
|
||||||
|
let items = ['Apple', 'Banana', 'Cherry', 'Date'];
|
||||||
|
spliceItems = () => {
|
||||||
|
items.splice(1, 2, 'Elderberry', 'Fig');
|
||||||
|
};
|
||||||
|
return <p>{items.join(', ')}</p>;
|
||||||
|
}
|
||||||
|
render(ArraySpliceState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>Apple, Banana, Cherry, Date</p>');
|
||||||
|
spliceItems();
|
||||||
|
expect(container.innerHTML).toBe('<p>Apple, Elderberry, Fig, Date</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly update and render an array state variable - filter operation', ({ container }) => {
|
||||||
|
let filterItems: () => void;
|
||||||
|
function ArrayFilterState() {
|
||||||
|
let items = ['Apple', 'Banana', 'Cherry', 'Date'];
|
||||||
|
filterItems = () => {
|
||||||
|
items = items.filter(item => item.length > 5);
|
||||||
|
};
|
||||||
|
return <p>{items.join(', ')}</p>;
|
||||||
|
}
|
||||||
|
render(ArrayFilterState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>Apple, Banana, Cherry, Date</p>');
|
||||||
|
filterItems();
|
||||||
|
expect(container.innerHTML).toBe('<p>Banana, Cherry</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly update and render an array state variable - map operation', ({ container }) => {
|
||||||
|
let mapItems: () => void;
|
||||||
|
function ArrayMapState() {
|
||||||
|
let items = ['apple', 'banana', 'cherry'];
|
||||||
|
mapItems = () => {
|
||||||
|
items = items.map(item => item.toUpperCase());
|
||||||
|
};
|
||||||
|
return <p>{items.join(', ')}</p>;
|
||||||
|
}
|
||||||
|
render(ArrayMapState, container);
|
||||||
|
expect(container.innerHTML).toBe('<p>apple, banana, cherry</p>');
|
||||||
|
mapItems();
|
||||||
|
expect(container.innerHTML).toBe('<p>APPLE, BANANA, CHERRY</p>');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,30 +1,30 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
*
|
*
|
||||||
* openInula is licensed under Mulan PSL v2.
|
* openInula is licensed under Mulan PSL v2.
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
*
|
*
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
*
|
*
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { test } from 'vitest';
|
import { test } from 'vitest';
|
||||||
|
|
||||||
interface DomTestContext {
|
interface DomTestContext {
|
||||||
container: HTMLDivElement;
|
container: HTMLDivElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define a new test type that extends the default test type and adds the container fixture.
|
// Define a new test type that extends the default test type and adds the container fixture.
|
||||||
export const domTest = test.extend<DomTestContext>({
|
export const domTest = test.extend<DomTestContext>({
|
||||||
container: async ({ task }, use) => {
|
container: async ({ task }, use) => {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
await use(container);
|
await use(container);
|
||||||
container.remove();
|
container.remove();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,325 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, vi } from 'vitest';
|
||||||
|
import { domTest as it } from './utils';
|
||||||
|
import { render, View } from '../src';
|
||||||
|
|
||||||
|
vi.mock('../src/scheduler', async () => {
|
||||||
|
return {
|
||||||
|
schedule: (task: () => void) => {
|
||||||
|
task();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Watch', () => {
|
||||||
|
it('Should correctly watch variable', ({ container }) => {
|
||||||
|
let increment: () => void;
|
||||||
|
function TrafficLight() {
|
||||||
|
let lightIndex = 0;
|
||||||
|
let index = 0;
|
||||||
|
increment = () => {
|
||||||
|
lightIndex = lightIndex + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => {
|
||||||
|
index = lightIndex;
|
||||||
|
});
|
||||||
|
return <div>{index}</div>;
|
||||||
|
}
|
||||||
|
render(TrafficLight, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>0</div>');
|
||||||
|
increment();
|
||||||
|
expect(container.innerHTML).toBe('<div>1</div>');
|
||||||
|
});
|
||||||
|
it('Should correctly watch and update based on string state changes', ({ container }) => {
|
||||||
|
let updateState: (newState: string) => void;
|
||||||
|
function StringComponent() {
|
||||||
|
let state = 'initial';
|
||||||
|
let watchedState = '';
|
||||||
|
updateState = (newState: string) => {
|
||||||
|
state = newState;
|
||||||
|
};
|
||||||
|
watch(() => {
|
||||||
|
watchedState = state;
|
||||||
|
});
|
||||||
|
return <div>{watchedState}</div>;
|
||||||
|
}
|
||||||
|
render(StringComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>initial</div>');
|
||||||
|
updateState('updated');
|
||||||
|
expect(container.innerHTML).toBe('<div>updated</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly watch and update based on number state changes', ({ container }) => {
|
||||||
|
let updateState: (newState: number) => void;
|
||||||
|
function NumberComponent() {
|
||||||
|
let state = 0;
|
||||||
|
let watchedState = 0;
|
||||||
|
updateState = (newState: number) => {
|
||||||
|
state = newState;
|
||||||
|
};
|
||||||
|
watch(() => {
|
||||||
|
watchedState = state;
|
||||||
|
});
|
||||||
|
return <div>{watchedState}</div>;
|
||||||
|
}
|
||||||
|
render(NumberComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>0</div>');
|
||||||
|
updateState(42);
|
||||||
|
expect(container.innerHTML).toBe('<div>42</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly watch and update based on boolean state changes', ({ container }) => {
|
||||||
|
let updateState: (newState: boolean) => void;
|
||||||
|
function BooleanComponent() {
|
||||||
|
let state = false;
|
||||||
|
let watchedState = false;
|
||||||
|
updateState = (newState: boolean) => {
|
||||||
|
state = newState;
|
||||||
|
};
|
||||||
|
watch(() => {
|
||||||
|
watchedState = state;
|
||||||
|
});
|
||||||
|
return <div>{watchedState.toString()}</div>;
|
||||||
|
}
|
||||||
|
render(BooleanComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>false</div>');
|
||||||
|
updateState(true);
|
||||||
|
expect(container.innerHTML).toBe('<div>true</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly watch and update based on array state changes', ({ container }) => {
|
||||||
|
let updateState: (newState: number[]) => void;
|
||||||
|
function ArrayComponent() {
|
||||||
|
let state: number[] = [];
|
||||||
|
let watchedState: number[] = [];
|
||||||
|
updateState = (newState: number[]) => {
|
||||||
|
state = newState;
|
||||||
|
};
|
||||||
|
watch(() => {
|
||||||
|
watchedState = [...state];
|
||||||
|
});
|
||||||
|
return <div>{watchedState.join(',')}</div>;
|
||||||
|
}
|
||||||
|
render(ArrayComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div></div>');
|
||||||
|
updateState([1, 2, 3]);
|
||||||
|
expect(container.innerHTML).toBe('<div>1,2,3</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly watch and update based on object state changes', ({ container }) => {
|
||||||
|
let updateState: (newState: { [key: string]: any }) => void;
|
||||||
|
function ObjectComponent() {
|
||||||
|
let state = {};
|
||||||
|
let watchedState = {};
|
||||||
|
updateState = (newState: { [key: string]: any }) => {
|
||||||
|
state = newState;
|
||||||
|
};
|
||||||
|
watch(() => {
|
||||||
|
watchedState = { ...state };
|
||||||
|
});
|
||||||
|
return <div>{JSON.stringify(watchedState)}</div>;
|
||||||
|
}
|
||||||
|
render(ObjectComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>{}</div>');
|
||||||
|
updateState({ key: 'value' });
|
||||||
|
expect(container.innerHTML).toBe('<div>{"key":"value"}</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly watch conditional expressions', ({ container }) => {
|
||||||
|
let updateX: (value: number) => void;
|
||||||
|
function ConditionalComponent() {
|
||||||
|
let x = 5;
|
||||||
|
let result = '';
|
||||||
|
updateX = (value: number) => {
|
||||||
|
x = value;
|
||||||
|
};
|
||||||
|
watch(() => {
|
||||||
|
result = x > 10 ? 'Greater than 10' : 'Less than or equal to 10';
|
||||||
|
});
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
render(ConditionalComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>Less than or equal to 10</div>');
|
||||||
|
updateX(15);
|
||||||
|
expect(container.innerHTML).toBe('<div>Greater than 10</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly watch template strings', ({ container }) => {
|
||||||
|
let updateName: (value: string) => void;
|
||||||
|
let updateAge: (value: number) => void;
|
||||||
|
function TemplateStringComponent() {
|
||||||
|
let name = 'John';
|
||||||
|
let age = 30;
|
||||||
|
let message = '';
|
||||||
|
updateName = (value: string) => {
|
||||||
|
name = value;
|
||||||
|
};
|
||||||
|
updateAge = (value: number) => {
|
||||||
|
age = value;
|
||||||
|
};
|
||||||
|
watch(() => {
|
||||||
|
message = `${name} is ${age} years old`;
|
||||||
|
});
|
||||||
|
return <div>{message}</div>;
|
||||||
|
}
|
||||||
|
render(TemplateStringComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>John is 30 years old</div>');
|
||||||
|
updateName('Jane');
|
||||||
|
updateAge(25);
|
||||||
|
expect(container.innerHTML).toBe('<div>Jane is 25 years old</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly watch arithmetic operations', ({ container }) => {
|
||||||
|
let updateX: (value: number) => void;
|
||||||
|
let updateY: (value: number) => void;
|
||||||
|
function ArithmeticComponent() {
|
||||||
|
let x = 5;
|
||||||
|
let y = 3;
|
||||||
|
let result = 0;
|
||||||
|
updateX = (value: number) => {
|
||||||
|
x = value;
|
||||||
|
};
|
||||||
|
updateY = (value: number) => {
|
||||||
|
y = value;
|
||||||
|
};
|
||||||
|
watch(() => {
|
||||||
|
result = x + y * 2 - x / y;
|
||||||
|
});
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
render(ArithmeticComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>9.333333333333334</div>');
|
||||||
|
updateX(10);
|
||||||
|
updateY(5);
|
||||||
|
expect(container.innerHTML).toBe('<div>18</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly watch property access and indexing', ({ container }) => {
|
||||||
|
let updateObj: (value: { [key: string]: any }) => void;
|
||||||
|
let updateArr: (value: number[]) => void;
|
||||||
|
function AccessComponent() {
|
||||||
|
let obj = { a: 1, b: 2 };
|
||||||
|
let arr = [1, 2, 3];
|
||||||
|
let result = 0;
|
||||||
|
updateObj = (value: { [key: string]: any }) => {
|
||||||
|
obj = value;
|
||||||
|
};
|
||||||
|
updateArr = (value: number[]) => {
|
||||||
|
arr = value;
|
||||||
|
};
|
||||||
|
watch(() => {
|
||||||
|
result = obj.a + arr[1];
|
||||||
|
});
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
render(AccessComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>3</div>');
|
||||||
|
updateObj({ a: 5, b: 6 });
|
||||||
|
updateArr([0, 10, 20]);
|
||||||
|
expect(container.innerHTML).toBe('<div>15</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly watch function calls', ({ container }) => {
|
||||||
|
let updateX: (value: number) => void;
|
||||||
|
function FunctionCallComponent() {
|
||||||
|
let x = 5;
|
||||||
|
let result = 0;
|
||||||
|
const square = (n: number) => n * n;
|
||||||
|
updateX = (value: number) => {
|
||||||
|
x = value;
|
||||||
|
};
|
||||||
|
watch(() => {
|
||||||
|
result = square(x);
|
||||||
|
});
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
render(FunctionCallComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>25</div>');
|
||||||
|
updateX(10);
|
||||||
|
expect(container.innerHTML).toBe('<div>100</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly watch various number operations', ({ container }) => {
|
||||||
|
let updateX: (value: number) => void;
|
||||||
|
function NumberOpsComponent() {
|
||||||
|
let x = 16;
|
||||||
|
let result = '';
|
||||||
|
updateX = (value: number) => {
|
||||||
|
x = value;
|
||||||
|
};
|
||||||
|
watch(() => {
|
||||||
|
result = `sqrt: ${Math.sqrt(x)}, floor: ${Math.floor(x / 3)}, pow: ${Math.pow(x, 2)}`;
|
||||||
|
});
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
render(NumberOpsComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>sqrt: 4, floor: 5, pow: 256</div>');
|
||||||
|
updateX(25);
|
||||||
|
expect(container.innerHTML).toBe('<div>sqrt: 5, floor: 8, pow: 625</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly watch map operations', ({ container }) => {
|
||||||
|
let updateArr: (value: number[]) => void;
|
||||||
|
function MapComponent() {
|
||||||
|
let arr = [1, 2, 3, 4, 5];
|
||||||
|
let result: number[] = [];
|
||||||
|
updateArr = (value: number[]) => {
|
||||||
|
arr = value;
|
||||||
|
};
|
||||||
|
watch(() => {
|
||||||
|
result = arr.map(x => x * 2);
|
||||||
|
});
|
||||||
|
return <div>{result.join(', ')}</div>;
|
||||||
|
}
|
||||||
|
render(MapComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>2, 4, 6, 8, 10</div>');
|
||||||
|
updateArr([10, 20, 30]);
|
||||||
|
expect(container.innerHTML).toBe('<div>20, 40, 60</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should correctly watch multiple variables', ({ container }) => {
|
||||||
|
let updateX: (value: number) => void;
|
||||||
|
let updateY: (value: string) => void;
|
||||||
|
let updateZ: (value: boolean) => void;
|
||||||
|
function MultiWatchComponent() {
|
||||||
|
let x = 5;
|
||||||
|
let y = 'hello';
|
||||||
|
let z = true;
|
||||||
|
let result = '';
|
||||||
|
updateX = (value: number) => {
|
||||||
|
x = value;
|
||||||
|
};
|
||||||
|
updateY = (value: string) => {
|
||||||
|
y = value;
|
||||||
|
};
|
||||||
|
updateZ = (value: boolean) => {
|
||||||
|
z = value;
|
||||||
|
};
|
||||||
|
watch(() => {
|
||||||
|
result = `x: ${x}, y: ${y}, z: ${z}`;
|
||||||
|
});
|
||||||
|
return <div>{result}</div>;
|
||||||
|
}
|
||||||
|
render(MultiWatchComponent, container);
|
||||||
|
expect(container.innerHTML).toBe('<div>x: 5, y: hello, z: true</div>');
|
||||||
|
updateX(10);
|
||||||
|
updateY('world');
|
||||||
|
updateZ(false);
|
||||||
|
expect(container.innerHTML).toBe('<div>x: 10, y: world, z: false</div>');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,14 +1,14 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"lib": ["ESNext", "DOM"],
|
"lib": ["ESNext", "DOM"],
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"esm": true
|
"esm": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,39 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
*
|
*
|
||||||
* openInula is licensed under Mulan PSL v2.
|
* openInula is licensed under Mulan PSL v2.
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
*
|
*
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
*
|
*
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// vitest.config.ts
|
// vitest.config.ts
|
||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
import inula from 'vite-plugin-inula-next';
|
import inula from '@openinula/vite-plugin-inula-next';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
esbuild: {
|
esbuild: {
|
||||||
jsx: 'preserve',
|
jsx: 'preserve',
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@openinula/next': path.resolve(__dirname, 'src'),
|
'@openinula/next': path.resolve(__dirname, 'src'),
|
||||||
},
|
},
|
||||||
conditions: ['dev'],
|
conditions: ['dev'],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// @ts-expect-error TODO: fix vite plugin interface is not compatible
|
inula({
|
||||||
inula(),
|
excludeFiles: '**/src/**/*{js,jsx,ts,tsx}',
|
||||||
],
|
}),
|
||||||
test: {
|
],
|
||||||
environment: 'jsdom', // or 'jsdom', 'node'
|
test: {
|
||||||
},
|
environment: 'jsdom', // or 'jsdom', 'node'
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"name": "inula-router-config",
|
|
||||||
"module": "./esm/connectRouter.js",
|
|
||||||
"main": "./cjs/connectRouter.js",
|
|
||||||
"types": "./@types/index.d.ts",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react-redux": "^6.0.0 || ^7.1.0",
|
|
||||||
"redux": "^3.6.0 || ^4.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"openinula"
|
"openinula"
|
||||||
],
|
],
|
||||||
"version": "0.0.4",
|
"version": "0.0.1",
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
"bugs": "",
|
"bugs": "",
|
||||||
"license": "MulanPSL2",
|
"license": "MulanPSL2",
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
# @openinlua/babel-api
|
# @openinlua/babel-api
|
||||||
A package that encapsulates the babel API for use in the transpiler.
|
A package that encapsulates the babel API for use in the transpiler.
|
||||||
|
|
||||||
To implement the dependency injection pattern, the package exports a function that registers the babel API in the
|
To implement the dependency injection pattern, the package exports a function that registers the babel API in the
|
||||||
transpiler.
|
transpiler.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { registerBabelAPI } from '@openinlua/babel-api';
|
import { registerBabelAPI } from '@openinlua/babel-api';
|
||||||
|
|
||||||
function plugin(api: typeof babel) {
|
function plugin(api: typeof babel) {
|
||||||
registerBabelAPI(api);
|
registerBabelAPI(api);
|
||||||
|
|
||||||
// Your babel plugin code here.
|
// Your babel plugin code here.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
And then you can import to use it.
|
And then you can import to use it.
|
||||||
> types can use as a `type` or as a `namespace` for the babel API.
|
> types can use as a `type` or as a `namespace` for the babel API.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { types as t } from '@openinlua/babel-api';
|
import { types as t } from '@openinlua/babel-api';
|
||||||
|
|
||||||
t.isIdentifier(node as t.Node);
|
t.isIdentifier(node as t.Node);
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import type babel from '@babel/core';
|
import type babel from '@babel/core';
|
||||||
|
|
||||||
// use .d.ts to satisfy the type check
|
// use .d.ts to satisfy the type check
|
||||||
export * as types from '@babel/types';
|
export * as types from '@babel/types';
|
||||||
|
|
||||||
export declare function register(api: typeof babel): void;
|
export declare function register(api: typeof babel): void;
|
||||||
export declare function getBabelApi(): typeof babel;
|
|
||||||
|
export declare function getBabelApi(): typeof babel;
|
||||||
|
|
||||||
|
export { traverse } from '@babel/core';
|
||||||
|
|
|
@ -1,38 +1,42 @@
|
||||||
/** @type {null | typeof import('@babel/core').types} */
|
/** @type {null | typeof import('@babel/core').types} */
|
||||||
let _t = null;
|
let _t = null;
|
||||||
/** @type {null | typeof import('@babel/core')} */
|
/** @type {null | typeof import('@babel/core')} */
|
||||||
let babelApi = null;
|
let babelApi = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@babel/core')} api
|
* @param {import('@babel/core')} api
|
||||||
*/
|
*/
|
||||||
export const register = api => {
|
export const register = api => {
|
||||||
babelApi = api;
|
babelApi = api;
|
||||||
_t = api.types;
|
_t = api.types;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {typeof import('@babel/core')}
|
* @returns {typeof import('@babel/core')}
|
||||||
*/
|
*/
|
||||||
export const getBabelApi = () => {
|
export const getBabelApi = () => {
|
||||||
if (!babelApi) {
|
if (!babelApi) {
|
||||||
throw new Error('Please call register() before using the babel api');
|
throw new Error('Please call register() before using the babel api');
|
||||||
}
|
}
|
||||||
return babelApi;
|
return babelApi;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const types = new Proxy(
|
export function traverse(node, visitor) {
|
||||||
{},
|
getBabelApi().traverse(node, visitor);
|
||||||
{
|
}
|
||||||
get: (_, p, receiver) => {
|
|
||||||
if (!_t) {
|
export const types = new Proxy(
|
||||||
throw new Error('Please call register() before using the babel types');
|
{},
|
||||||
}
|
{
|
||||||
|
get: (_, p, receiver) => {
|
||||||
if (p in _t) {
|
if (!_t) {
|
||||||
return Reflect.get(_t, p, receiver);
|
throw new Error('Please call register() before using the babel types');
|
||||||
}
|
}
|
||||||
return undefined;
|
|
||||||
},
|
if (p in _t) {
|
||||||
}
|
return Reflect.get(_t, p, receiver);
|
||||||
);
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -1,14 +1,25 @@
|
||||||
# babel-preset-inula-next
|
# babel-preset-inula-next
|
||||||
|
|
||||||
## 0.0.3
|
## 0.0.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
- Updated dependencies
|
- support hook
|
||||||
- @openinula/class-transformer@0.0.2
|
- Updated dependencies
|
||||||
|
- @openinula/reactivity-parser@0.0.2
|
||||||
## 0.0.2
|
- @openinula/view-generator@1.0.0
|
||||||
|
- @openinula/jsx-view-parser@0.0.2
|
||||||
### Patch Changes
|
- @openinula/babel-api@1.0.1
|
||||||
|
|
||||||
- 2f9d373: feat: change babel import
|
## 0.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- @openinula/class-transformer@0.0.2
|
||||||
|
|
||||||
|
## 0.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 2f9d373: feat: change babel import
|
||||||
|
|
|
@ -1,62 +1,60 @@
|
||||||
{
|
{
|
||||||
"name": "babel-preset-inula-next",
|
"name": "@openinula/babel-preset-inula-next",
|
||||||
"version": "0.0.3",
|
"version": "0.0.0-SNAPSHOT",
|
||||||
"author": {
|
"keywords": [
|
||||||
"name": "IanDx",
|
"Inula-Next",
|
||||||
"email": "iandxssxx@gmail.com"
|
"babel-preset"
|
||||||
},
|
],
|
||||||
"keywords": [
|
"license": "MIT",
|
||||||
"dlight.js",
|
"files": [
|
||||||
"babel-preset"
|
"dist"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"type": "module",
|
||||||
"files": [
|
"main": "dist/index.js",
|
||||||
"dist"
|
"module": "dist/index.js",
|
||||||
],
|
"typings": "dist/index.d.ts",
|
||||||
"type": "module",
|
"scripts": {
|
||||||
"main": "dist/index.cjs",
|
"build": "tsup --sourcemap",
|
||||||
"module": "dist/index.js",
|
"type-check": "tsc --noEmit",
|
||||||
"typings": "dist/index.d.ts",
|
"test": "vitest run"
|
||||||
"scripts": {
|
},
|
||||||
"build": "tsup --sourcemap",
|
"devDependencies": {
|
||||||
"type-check": "tsc --noEmit",
|
"@types/babel__core": "^7.20.5",
|
||||||
"test": "vitest"
|
"@types/node": "^20.10.5",
|
||||||
},
|
"@vitest/coverage-v8": "^1.6.0",
|
||||||
"devDependencies": {
|
"sinon": "^18.0.0",
|
||||||
"@types/babel__core": "^7.20.5",
|
"tsup": "^6.7.0",
|
||||||
"@types/node": "^20.10.5",
|
"typescript": "^5.3.2"
|
||||||
"tsup": "^6.7.0",
|
},
|
||||||
"typescript": "^5.3.2"
|
"dependencies": {
|
||||||
},
|
"@babel/core": "^7.23.3",
|
||||||
"dependencies": {
|
"@babel/generator": "^7.23.6",
|
||||||
"@babel/core": "^7.23.3",
|
"@babel/parser": "^7.24.4",
|
||||||
"@babel/generator": "^7.23.6",
|
"@babel/plugin-syntax-decorators": "^7.23.3",
|
||||||
"@babel/parser": "^7.24.4",
|
"@babel/plugin-syntax-jsx": "7.16.7",
|
||||||
"@babel/plugin-syntax-decorators": "^7.23.3",
|
"@babel/plugin-syntax-typescript": "^7.23.3",
|
||||||
"@babel/plugin-syntax-jsx": "7.16.7",
|
"@babel/traverse": "^7.24.1",
|
||||||
"@babel/plugin-syntax-typescript": "^7.23.3",
|
"@babel/types": "^7.24.0",
|
||||||
"@babel/traverse": "^7.24.1",
|
"@openinula/babel-api": "workspace:*",
|
||||||
"@babel/types": "^7.24.0",
|
"@openinula/jsx-view-parser": "workspace:*",
|
||||||
"@openinula/reactivity-parser": "workspace:*",
|
"@openinula/reactivity-parser": "workspace:*",
|
||||||
"@openinula/view-parser": "workspace:*",
|
"@openinula/view-generator": "workspace:*",
|
||||||
"@types/babel-types": "^7.0.15",
|
"@types/babel-types": "^7.0.15",
|
||||||
"@types/babel__generator": "^7.6.8",
|
"@types/babel__generator": "^7.6.8",
|
||||||
"@types/babel__parser": "^7.1.1",
|
"@types/babel__parser": "^7.1.1",
|
||||||
"@types/babel__traverse": "^7.6.8",
|
"@types/babel__traverse": "^7.6.8",
|
||||||
"@openinula/jsx-view-parser": "workspace:*",
|
"minimatch": "^9.0.3",
|
||||||
"@openinula/babel-api": "workspace:*",
|
"vitest": "^1.4.0"
|
||||||
"minimatch": "^9.0.3",
|
},
|
||||||
"vitest": "^1.4.0"
|
"tsup": {
|
||||||
},
|
"entry": [
|
||||||
"tsup": {
|
"src/index.ts"
|
||||||
"entry": [
|
],
|
||||||
"src/index.ts"
|
"format": [
|
||||||
],
|
"cjs",
|
||||||
"format": [
|
"esm"
|
||||||
"cjs",
|
],
|
||||||
"esm"
|
"clean": true,
|
||||||
],
|
"dts": true
|
||||||
"clean": true,
|
}
|
||||||
"dts": true
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,88 +1,85 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
*
|
*
|
||||||
* openInula is licensed under Mulan PSL v2.
|
* openInula is licensed under Mulan PSL v2.
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
*
|
*
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
*
|
*
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NodePath } from '@babel/core';
|
import { NodePath } from '@babel/core';
|
||||||
import { LifeCycle, Visitor } from '../types';
|
import { LifeCycle, Visitor } from '../types';
|
||||||
import { addLifecycle, addWatch } from '../nodeFactory';
|
import { types as t } from '@openinula/babel-api';
|
||||||
import { types as t } from '@openinula/babel-api';
|
import { DID_MOUNT, DID_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../../constants';
|
||||||
import { ON_MOUNT, ON_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../../constants';
|
import { extractFnFromMacro, getFnBodyPath } from '../../utils';
|
||||||
import { extractFnFromMacro, getFnBodyPath } from '../../utils';
|
import { extractFnBody } from '../utils';
|
||||||
import { getDependenciesFromNode } from '@openinula/reactivity-parser';
|
|
||||||
import { reactivityFuncNames } from '../../const';
|
function isLifeCycleName(name: string): name is LifeCycle {
|
||||||
|
return [WILL_MOUNT, DID_MOUNT, WILL_UNMOUNT, DID_UNMOUNT].includes(name);
|
||||||
function isLifeCycleName(name: string): name is LifeCycle {
|
}
|
||||||
return [WILL_MOUNT, ON_MOUNT, WILL_UNMOUNT, ON_UNMOUNT].includes(name);
|
|
||||||
}
|
/**
|
||||||
|
* Analyze the functional macro in the function component
|
||||||
/**
|
* 1. lifecycle
|
||||||
* Analyze the functional macro in the function component
|
* 1. willMount
|
||||||
* 1. lifecycle
|
* 2. didMount
|
||||||
* 1. willMount
|
* 3. willUnMount
|
||||||
* 2. onMount
|
* 4. didUnmount
|
||||||
* 3. willUnMount
|
* 2. watch
|
||||||
* 4. onUnmount
|
*/
|
||||||
* 2. watch
|
export function functionalMacroAnalyze(): Visitor {
|
||||||
*/
|
return {
|
||||||
export function functionalMacroAnalyze(): Visitor {
|
ExpressionStatement(path: NodePath<t.ExpressionStatement>, { builder }) {
|
||||||
return {
|
const expression = path.get('expression');
|
||||||
ExpressionStatement(path: NodePath<t.ExpressionStatement>, ctx) {
|
if (expression.isCallExpression()) {
|
||||||
const expression = path.get('expression');
|
const callee = expression.get('callee');
|
||||||
if (expression.isCallExpression()) {
|
if (callee.isIdentifier()) {
|
||||||
const callee = expression.get('callee');
|
const calleeName = callee.node.name;
|
||||||
if (callee.isIdentifier()) {
|
// lifecycle
|
||||||
const calleeName = callee.node.name;
|
if (isLifeCycleName(calleeName)) {
|
||||||
// lifecycle
|
const fnPath = extractFnFromMacro(expression, calleeName);
|
||||||
if (isLifeCycleName(calleeName)) {
|
builder.addLifecycle(calleeName, extractFnBody(fnPath.node));
|
||||||
const fnNode = extractFnFromMacro(expression, calleeName);
|
return;
|
||||||
addLifecycle(ctx.current, calleeName, getFnBodyPath(fnNode).node);
|
}
|
||||||
return;
|
|
||||||
}
|
// watch
|
||||||
|
if (calleeName === WATCH) {
|
||||||
// watch
|
const fnPath = extractFnFromMacro(expression, WATCH);
|
||||||
if (calleeName === WATCH) {
|
const depsPath = getWatchDeps(expression);
|
||||||
const fnPath = extractFnFromMacro(expression, WATCH);
|
|
||||||
const depsPath = getWatchDeps(expression);
|
const dependency = builder.getDependency((depsPath ?? fnPath).node);
|
||||||
|
if (dependency.allDepBits.length) {
|
||||||
const dependency = getDependenciesFromNode(
|
builder.addWatch(fnPath, dependency);
|
||||||
(depsPath ?? fnPath).node,
|
} else {
|
||||||
ctx.current._reactiveBitMap,
|
builder.addLifecycle(WILL_MOUNT, extractFnBody(fnPath.node));
|
||||||
reactivityFuncNames
|
}
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
addWatch(ctx.current, fnPath, dependency);
|
}
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
}
|
builder.addWillMount(path.node);
|
||||||
}
|
},
|
||||||
|
};
|
||||||
ctx.unhandledNode.push(path.node);
|
}
|
||||||
},
|
|
||||||
};
|
function getWatchDeps(callExpression: NodePath<t.CallExpression>) {
|
||||||
}
|
const args = callExpression.get('arguments');
|
||||||
|
if (!args[1]) {
|
||||||
function getWatchDeps(callExpression: NodePath<t.CallExpression>) {
|
return null;
|
||||||
const args = callExpression.get('arguments');
|
}
|
||||||
if (!args[1]) {
|
|
||||||
return null;
|
let deps: null | NodePath<t.ArrayExpression> = null;
|
||||||
}
|
if (args[1].isArrayExpression()) {
|
||||||
|
deps = args[1];
|
||||||
let deps: null | NodePath<t.ArrayExpression> = null;
|
} else {
|
||||||
if (args[1].isArrayExpression()) {
|
console.error('watch deps should be an array expression');
|
||||||
deps = args[1];
|
}
|
||||||
} else {
|
return deps;
|
||||||
console.error('watch deps should be an array expression');
|
}
|
||||||
}
|
|
||||||
return deps;
|
|
||||||
}
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue