mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: add fragment support for hot reloading and fix some stuff (#659)
This commit is contained in:
parent
1a3c1e9e52
commit
591212a56a
6 changed files with 226 additions and 78 deletions
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
! tailwindcss v3.1.8 | MIT License | https://tailwindcss.com
|
||||
! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
|
@ -30,6 +30,7 @@
|
|||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
|
@ -44,6 +45,8 @@ html {
|
|||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -410,54 +413,13 @@ video {
|
|||
height: auto;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-backdrop {
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
|
@ -569,11 +531,39 @@ video {
|
|||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.rounded-sm {
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.bg-amber-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(217 119 6 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(2 132 199 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-300 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(125 211 252 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(14 165 233 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-amber-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(245 158 11 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
@ -605,6 +595,10 @@ video {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
|
@ -615,6 +609,41 @@ video {
|
|||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(239 68 68 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-200 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(254 202 202 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(14 165 233 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(125 211 252 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(3 105 161 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(7 89 133 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(153 27 27 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-sky-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(3 105 161 / var(--tw-bg-opacity));
|
||||
|
|
|
@ -63,6 +63,8 @@ pub struct ComponentRepr {
|
|||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ComponentRepr {
|
||||
|
@ -205,6 +207,8 @@ impl ComponentRepr {
|
|||
children: Vec::with_capacity(1),
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ pub struct Fragment {
|
|||
id: HydrationKey,
|
||||
/// The nodes contained in the fragment.
|
||||
pub nodes: Vec<View>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>,
|
||||
}
|
||||
|
||||
impl FromIterator<View> for Fragment {
|
||||
|
@ -52,7 +54,12 @@ impl Fragment {
|
|||
|
||||
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
|
||||
pub fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
|
||||
Self { id, nodes }
|
||||
Self {
|
||||
id,
|
||||
nodes,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gives access to the [View] children contained within the fragment.
|
||||
|
@ -64,6 +71,13 @@ impl Fragment {
|
|||
pub fn id(&self) -> &HydrationKey {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
/// Adds an optional marker indicating the view macro source.
|
||||
pub fn with_view_marker(mut self, marker: impl Into<String>) -> Self {
|
||||
self.view_marker = Some(marker.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for Fragment {
|
||||
|
@ -71,6 +85,11 @@ impl IntoView for Fragment {
|
|||
fn into_view(self, cx: leptos_reactive::Scope) -> View {
|
||||
let mut frag = ComponentRepr::new_with_id("", self.id.clone());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
frag.view_marker = self.view_marker;
|
||||
}
|
||||
|
||||
frag.children = self.nodes;
|
||||
|
||||
frag.into_view(cx)
|
||||
|
|
|
@ -237,12 +237,17 @@ impl View {
|
|||
};
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
let content = format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
HydrationCtx::to_string(&node.id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&node.id, true),
|
||||
name = to_kebab_case(&node.name)
|
||||
).into()
|
||||
);
|
||||
if let Some(id) = node.view_marker {
|
||||
format!("<!--leptos-view|{id}|open-->{content}<!--leptos-view|{id}|close-->").into()
|
||||
} else {
|
||||
content.into()
|
||||
}
|
||||
} else {
|
||||
format!(
|
||||
r#"{}<!--hk={}-->"#,
|
||||
|
|
|
@ -2,7 +2,7 @@ console.log("[HOT RELOADING] Connected to server.");
|
|||
function patch(json) {
|
||||
try {
|
||||
const views = JSON.parse(json);
|
||||
for ([id, patches] of views) {
|
||||
for (const [id, patches] of views) {
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
|
||||
open = `leptos-view|${id}|open`,
|
||||
close = `leptos-view|${id}|close`;
|
||||
|
@ -16,10 +16,7 @@ function patch(json) {
|
|||
}
|
||||
}
|
||||
// build tree of current actual children
|
||||
const range = new Range();
|
||||
range.setStartAfter(start);
|
||||
range.setEndBefore(end);
|
||||
const actualChildren = buildActualChildren(start.parentElement, range);
|
||||
const actualChildren = childrenFromRange(start.parentElement, start, end);
|
||||
const actions = [];
|
||||
|
||||
// build up the set of actions
|
||||
|
@ -100,26 +97,49 @@ function patch(json) {
|
|||
}
|
||||
})
|
||||
} else if (action.InsertChild) {
|
||||
const newChild = fromReplacementNode(action.InsertChild.child, actualChildren),
|
||||
before = child.children[action.InsertChild.before];
|
||||
const newChild = fromReplacementNode(action.InsertChild.child, actualChildren);
|
||||
let children = [];
|
||||
if(child.children) {
|
||||
children = child.children;
|
||||
} else if (child.start && child.end) {
|
||||
children = childrenFromRange(child.node || child.start.parentElement, start, end);
|
||||
} else {
|
||||
console.warn("InsertChildAfter could not build children.");
|
||||
}
|
||||
const before = children[action.InsertChild.before];
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > InsertChild", child, child.node, action.InsertChild, " before ", before);
|
||||
if (!before) {
|
||||
if (!before && child.node) {
|
||||
child.node.appendChild(newChild);
|
||||
} else {
|
||||
child.node.insertBefore(newChild, (before.node || before.start));
|
||||
let node = child.node || child.end.parentElement;
|
||||
const reference = before ? before.node || before.start : child.end;
|
||||
node.insertBefore(newChild, reference);
|
||||
}
|
||||
})
|
||||
} else if (action.InsertChildAfter) {
|
||||
const newChild = fromReplacementNode(action.InsertChildAfter.child, actualChildren),
|
||||
after = child.children[action.InsertChildAfter.after];
|
||||
const newChild = fromReplacementNode(action.InsertChildAfter.child, actualChildren);
|
||||
let children = [];
|
||||
if(child.children) {
|
||||
children = child.children;
|
||||
} else if (child.start && child.end) {
|
||||
children = childrenFromRange(child.node || child.start.parentElement, start, end);
|
||||
} else {
|
||||
console.warn("InsertChildAfter could not build children.");
|
||||
}
|
||||
const after = children[action.InsertChildAfter.after];
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > InsertChildAfter", child, child.node, action.InsertChildAfter, " after ", after);
|
||||
console.log("newChild is ", newChild);
|
||||
if (!after || !(after.node || after.start).nextSibling) {
|
||||
if (child.node && (!after || !(after.node || after.start).nextSibling)) {
|
||||
child.node.appendChild(newChild);
|
||||
} else {
|
||||
child.node.insertBefore(newChild, (after.node || after.start).nextSibling);
|
||||
} else {
|
||||
const node = child.node || child.end;
|
||||
const parent = node.nodeType === Node.COMMENT_NODE ? node.parentNode : node;
|
||||
if(!after) {
|
||||
parent.appendChild(newChild);
|
||||
} else {
|
||||
parent.insertBefore(newChild, (after.node || after.start).nextSibling);
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
@ -138,7 +158,6 @@ function patch(json) {
|
|||
}
|
||||
|
||||
function fromReplacementNode(node, actualChildren) {
|
||||
console.log("fromReplacementNode", node, actualChildren);
|
||||
if (node.Html) {
|
||||
return fromHTML(node.Html);
|
||||
}
|
||||
|
@ -164,7 +183,6 @@ function patch(json) {
|
|||
actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0],
|
||||
node.Path
|
||||
);
|
||||
console.log("fromReplacementNode", child, "\n", node, actualChildren);
|
||||
if (child) {
|
||||
let childNode = child.node;
|
||||
if (!childNode) {
|
||||
|
@ -192,13 +210,13 @@ function patch(json) {
|
|||
}
|
||||
}
|
||||
|
||||
function buildActualChildren(element, range) {
|
||||
function buildActualChildren(element, range) {
|
||||
const walker = document.createTreeWalker(
|
||||
element,
|
||||
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT,
|
||||
{
|
||||
acceptNode(node) {
|
||||
return node.parentNode == element && (!range || range.isPointInRange(node, 0))
|
||||
return node.parentNode == element && (!range || range.isPointInRange(node, 0));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -234,22 +252,62 @@ function patch(json) {
|
|||
});
|
||||
} else if (walker.currentNode.textContent.trim() == "<DynChild>") {
|
||||
let start = walker.currentNode;
|
||||
while (walker.currentNode.textContent.trim() !== "</DynChild>") {
|
||||
walker.nextNode();
|
||||
}
|
||||
let end = walker.currentNode;
|
||||
let depth = 1;
|
||||
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.trim() == "</DynChild>") {
|
||||
depth--;
|
||||
} else if (walker.currentNode.textContent.trim() == "<DynChild>") {
|
||||
depth++;
|
||||
}
|
||||
|
||||
if(depth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let end = walker.currentNode;
|
||||
actualChildren.push({
|
||||
type: "dyn-child",
|
||||
start, end
|
||||
});
|
||||
} else if (walker.currentNode.textContent.trim() == "<>") {
|
||||
let start = walker.currentNode;
|
||||
let depth = 1;
|
||||
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.trim() == "</>") {
|
||||
depth--;
|
||||
} else if (walker.currentNode.textContent.trim() == "<>") {
|
||||
depth++;
|
||||
}
|
||||
|
||||
if(depth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let end = walker.currentNode;
|
||||
actualChildren.push({
|
||||
type: "fragment",
|
||||
children: childrenFromRange(start.parentElement, start, end),
|
||||
start, end
|
||||
});
|
||||
} else if (walker.currentNode.textContent.trim().startsWith("<")) {
|
||||
let componentName = walker.currentNode.textContent.trim();
|
||||
let endMarker = componentName.replace("<", "</");
|
||||
let depth = 1;
|
||||
let start = walker.currentNode;
|
||||
while (walker.currentNode.textContent.trim() !== endMarker) {
|
||||
walker.nextSibling();
|
||||
}
|
||||
let end = walker.currentNode;
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.trim() == endMarker) {
|
||||
depth--;
|
||||
} else if (walker.currentNode.textContent.trim() == componentName) {
|
||||
depth++;
|
||||
}
|
||||
|
||||
if(depth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let end = walker.currentNode;
|
||||
actualChildren.push({
|
||||
type: "component",
|
||||
start, end
|
||||
|
@ -271,12 +329,22 @@ function patch(json) {
|
|||
return childAtPath(next, rest);
|
||||
} else if (path == [0]) {
|
||||
return element;
|
||||
} else {
|
||||
} else if (element.start && element.end) {
|
||||
const actualChildren = childrenFromRange(element.node || element.start.parentElement, element.start, element.end);
|
||||
return childAtPath({ children: actualChildren }, path);
|
||||
} else {
|
||||
console.warn("[HOT RELOADING] Child at ", path, "not found in ", element);
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
function childrenFromRange(parent, start, end) {
|
||||
const range = new Range();
|
||||
range.setStartAfter(start);
|
||||
range.setEndBefore(end);
|
||||
return buildActualChildren(parent, range);
|
||||
}
|
||||
|
||||
function fromHTML(html) {
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = html;
|
||||
|
|
|
@ -165,6 +165,7 @@ pub(crate) fn render_view(
|
|||
Span::call_site(),
|
||||
nodes,
|
||||
global_class,
|
||||
call_site,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
|
@ -189,6 +190,7 @@ pub(crate) fn render_view(
|
|||
true,
|
||||
TagType::Unknown,
|
||||
global_class,
|
||||
call_site,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -206,6 +208,7 @@ fn root_node_to_tokens_ssr(
|
|||
Span::call_site(),
|
||||
&fragment.children,
|
||||
global_class,
|
||||
view_marker,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
|
||||
Node::Text(node) => {
|
||||
|
@ -232,7 +235,13 @@ fn fragment_to_tokens_ssr(
|
|||
_span: Span,
|
||||
nodes: &[Node],
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> TokenStream {
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let nodes = nodes.iter().map(|node| {
|
||||
let node = root_node_to_tokens_ssr(cx, node, global_class, None);
|
||||
quote! {
|
||||
|
@ -244,6 +253,7 @@ fn fragment_to_tokens_ssr(
|
|||
leptos::Fragment::lazy(|| vec![
|
||||
#(#nodes),*
|
||||
])
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -638,6 +648,7 @@ fn fragment_to_tokens(
|
|||
lazy: bool,
|
||||
parent_type: TagType,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> TokenStream {
|
||||
let nodes = nodes.iter().map(|node| {
|
||||
let node = node_to_tokens(cx, node, parent_type, global_class, None);
|
||||
|
@ -646,12 +657,20 @@ fn fragment_to_tokens(
|
|||
#node.into_view(#cx)
|
||||
}
|
||||
});
|
||||
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
if lazy {
|
||||
quote! {
|
||||
{
|
||||
leptos::Fragment::lazy(|| vec![
|
||||
#(#nodes),*
|
||||
])
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -660,6 +679,7 @@ fn fragment_to_tokens(
|
|||
leptos::Fragment::new(vec![
|
||||
#(#nodes),*
|
||||
])
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -680,6 +700,7 @@ fn node_to_tokens(
|
|||
true,
|
||||
parent_type,
|
||||
global_class,
|
||||
view_marker,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) => quote! {},
|
||||
Node::Text(node) => {
|
||||
|
@ -772,6 +793,7 @@ fn element_to_tokens(
|
|||
true,
|
||||
parent_type,
|
||||
global_class,
|
||||
None,
|
||||
),
|
||||
Node::Text(node) => {
|
||||
let value = node.value.as_ref();
|
||||
|
@ -1038,6 +1060,7 @@ pub(crate) fn component_to_tokens(
|
|||
true,
|
||||
TagType::Unknown,
|
||||
global_class,
|
||||
None,
|
||||
);
|
||||
|
||||
let clonables = items_to_clone
|
||||
|
|
Loading…
Reference in a new issue