feat: write reflected attributes when the prop changes, not on the next render
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 22/100
- Loại issue
- Tính năng
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- typescript
- Lĩnh vực
- frontend
Hướng nghiên cứu
Start from the prop setter path that handles @Prop({ reflect: true }), and from where reflected props are copied onto the host during render, as the issue describes. The change is to write the attribute at set time, only when the value changes and the element is connected, before @Watch handlers run. Reproduce the lag with the my-toggle example: after el.open = true, the attribute should already be present. Done means the attribute matches the property synchronously, with no change to SSR or to props that reflect only on first render, and the opt-in design is agreed with maintainers first.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Prerequisites
- I have read the Contributing Guidelines.
- I agree to follow the Code of Conduct.
- I have searched for existing issues that already include this feature request, without success.
Describe the Feature Request
Summary
A @Prop({ reflect: true }) updates its host attribute only when the component re-renders, and Stencil batches re-renders to the next animation frame. Between the prop change and that frame, the element's property and attribute disagree. Native elements don't behave this way: dialog.open = true and button.disabled = true update their attributes immediately.
Tying reflection to rendering is a reasonable design choice for a render-batching runtime. But reflection is a DOM contract, not a rendering detail: CSS, event listeners, the browser and application code read the attribute as the element's current state. When they read it inside that window, they see the previous value.
This request is to write reflected attributes when the prop changes, either by default or behind an opt-in.
Describe the Use Case
Reproduction
import { Component, Prop, h } from '@stencil/core';
@Component({ tag: 'my-toggle', encapsulation: { type: 'shadow' } })
export class MyToggle {
@Prop({ reflect: true }) open = false;
render() {
return <slot />;
}
}
const el = document.querySelector('my-toggle'); // defined and rendered
el.open = true;
el.hasAttribute('open'); // false
await Promise.resolve();
el.hasAttribute('open'); // false
await new Promise(r => setTimeout(r, 0));
el.hasAttribute('open'); // false
await new Promise(r => requestAnimationFrame(r));
el.hasAttribute('open'); // true
Same result on 4.45.3 (dist-custom-elements) and 5.0.0-rc.1 (standalone), in Chromium and WebKit. The attribute is written during render, where the reflected props are copied onto the host vnode, and a prop change reaches render through the scheduled
When the current behavior fails
The lag is invisible as long as nothing reads the attribute before the next n ways something does. Each was checked on 5.0.0-rc.1 in Chromium and WebKit.
1. A change event fires before the attribute follows. A chip emits an evusual way to announce a prop change:
@Component({ tag: 'my-chip', encapsulation: { type: 'shadow' } })
export class MyChip {
@Prop({ reflect: true }) selected = false;
@Event() selectedChange!: EventEmitter<boolean>;
@Watch('selected')
onSelectedChange(selected: boolean) {
this.selectedChange.emit(selected);
}
render() {
return <slot />;
}
}
A page shows how many filters are selected:
filters.addEventListener('selectedChange', () => {
counter.textContent = filters.querySelectorAll('my-chip[selected]').length;
});
Select chip A, then chip B, then unselect chip A. The listener counts 0, 1, 2 instead of 1, 2, 1: it always sees the state before the change that fired the event. The listener runs
in the same task as the prop change, before the attribute is reflected.
2. The browser itself reads the attribute. For a form-associated custom ves the disabled state from the disabled attribute:
@Component({ tag: 'my-field', encapsulation: { type: 'shadow' } })
export class MyField {
@AttachInternals() internals!: ElementInternals;
@Prop({ reflect: true }) disabled = false;
render() {
return <slot />;
}
}
// el.disabled was true, and the component has rendered since
el.disabled = false;
el.disabled; // false
el.matches(':disabled'); // true: the attribute is still there
el.click(); // no click event is dispatched: the browser treats the element as disabled
After the next frame, :disabled stops matching and click() dispatches noe component can work around this by reading the property, because the readeris the browser. Re-enabling a control and activating it in the same task, common in programmatic flows and tests, silently fails.
3. Application code that treats the attribute as state.
el.open = true;
el.matches('[open]'); // false
document.querySelectorAll('my-toggle[open]').length; // doesn't include el
Test assertions written against the attribute right after setting the prop fail the same way unless they wait for a frame.
Current workarounds
- Write the attribute by hand in a
@Watch, before emitting events or touching native APIs. This works, and Stencil's later reflection writes the same value. But every
component has to repeat Stencil's serialization rules (a boolean becomes""'s easy to miss a prop. It also has to usesetAttribute/removeAttribute,because mock-doc has notoggleAttribute()`, so the same code still runs during SSR. taskQueue: 'immediate'. This makes reflection synchronous by renderiprop change. It's global, so it changes update timing for every component:setting several props runs several renders instead of one batched render, and components that rely on batching behave differently. It isn't a practical fix for a reflection problem.
Describe Preferred Solution
Proposal
Write the reflected attribute in the prop setter, as part of the prop change:
- Only when the value changes. The attribute write calls
attributeChangedCallback, which already ignores a value equal to the reflected prop, so there's no loop. - Only when the element is connected. Custom elements must not add attributes in their constructor. Props set before connection can keep reflecting on first render, as they do today.
- Before
@Watchhandlers run, so watchers, and the events they emit, see the host attribute and the property agree. - With the same serialization the render-time reflection uses, and the same rules for complex types (not reflected).
- Rendering keeps writing the attribute as it does now. The write is idempotent, and SSR is unaffected.
This could be opt-in per prop, for example @Prop({ reflect: 'sync' }), or a config flag, so existing projects see no change. It could become the default in a later major.
Why not a microtask? All three cases above read the attribute in the same task as the prop change: an event listener, the browser's click() check, and code right after the assignment. A microtask deferral would stop stale attribute styles from being painted, but every one of these readers would still see the old value. Writing the attribute immediately matches native elements and fixes all three.
Describe Alternatives
No response
Related Code
No response
Additional Information
No response
- Ngôn ngữ chính
- TypeScript
- Star
- 13.2k
- Fork
- 865
- Merge trung bình
- 2 ngày 16 giờ
- Pull request đã merge (30 ngày)
- 29
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của stenciljs/core
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày
-
bug(mock-doc): `Element.toggleAttribute()` is missing, so components that use it fail during SSRĐang mởtriage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Stencil FOUC styles should use @layer to avoid cascade conflicts with React 19Có thể đã có người làm @johnjenkins đã nhận 230 ngày trước. Đang mởStencil v5
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 45/100
stenciljs/core#7016 · 1 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 35/100
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của stenciljs/core
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
AykutSarac/jsoncrack.com#616 ·
-
nip27: long hashtag swallows the rest of the content, lone # gives an empty hashtagCó thể đã có người làm @bardrop đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
nbd-wtf/nostr-tools#578 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
backnotprop/plannotator#3427 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
microsoft/Power-CAT-Copilot-Studio-Kit#942 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[BUG]: Duplicate preposition in heading and unformatted feedback text on Strengthen Argument pageĐang mởbug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
AOSSIE-Org/DebateAI#801 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày