Skip to content

Add attribute component:navigate#49

Open
MasterZydra wants to merge 2 commits intowire-elements:2.xfrom
MasterZydra:wire-navigate
Open

Add attribute component:navigate#49
MasterZydra wants to merge 2 commits intowire-elements:2.xfrom
MasterZydra:wire-navigate

Conversation

@MasterZydra
Copy link
Contributor

Usage

<a href="" component:navigate data-component="MyComponent" data-params='{someParam: 42}'>Page 2</a>

It replaces the attributes data-component and data-params in the closest livewire tag.

(Same as #15 but reopened after changing the branch name)

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new component:navigate attribute feature to wire-extender that allows anchor tags to dynamically load Livewire components without full page navigation. When clicked, elements with this attribute will replace the component and params in the nearest livewire parent element and re-render the component.

Changes:

  • Adds click event handling for elements with component:navigate attribute
  • Implements component switching by updating data-component and data-params on parent livewire elements
  • Sets up automatic event listener registration using Livewire hooks and MutationObserver

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

let component = event.target.getAttribute('data-component');
let params = event.target.getAttribute('data-params');

let livewireElem = event.target.closest("livewire");
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The closest() method can return null if no matching ancestor element is found. If the clicked element is not inside a livewire tag, this will cause a TypeError when trying to call setAttribute on null at lines 135-136. Add a null check and handle the case where no livewire element is found.

Suggested change
let livewireElem = event.target.closest("livewire");
let livewireElem = event.target.closest("livewire");
if (!livewireElem) {
console.warn('component:navigate click outside of a <livewire> element; navigation aborted.');
return;
}

Copilot uses AI. Check for mistakes.

// Register click event for all elements with attribute "component:navigate"
const componentNavigateElementUpdated = ({el}) => {
if (el == undefined) {
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use strict equality (===) instead of loose equality (==) to avoid type coercion issues. The codebase appears to use === in other comparisons, and this is a JavaScript best practice.

Suggested change
if (el == undefined) {
if (el === undefined) {

Copilot uses AI. Check for mistakes.
Comment on lines +128 to +168
const componentNavigateClick = (event) => {
event.preventDefault();

let component = event.target.getAttribute('data-component');
let params = event.target.getAttribute('data-params');

let livewireElem = event.target.closest("livewire");
livewireElem.setAttribute('data-component', component);
livewireElem.setAttribute('data-params', params ?? '');

renderComponents([{name: component, params: params}]);
};

// Register click event for all elements with attribute "component:navigate"
const componentNavigateElementUpdated = ({el}) => {
if (el == undefined) {
document.querySelectorAll('[component\\:navigate]').forEach(
(elem) => elem.addEventListener('click', componentNavigateClick)
);
return;
}

if (el.hasAttribute('component:navigate')) {
el.addEventListener('click', componentNavigateClick);
return;
}
};

// Trigger event for every livewire update cycle
window.Livewire.hook('morph.updated', componentNavigateElementUpdated);

// Use observer to detect if the component has been replaced
const componentNavigateObserver = new MutationObserver( () => {
componentNavigateElementUpdated({ undefined });
});
document.querySelectorAll('livewire').forEach((elem) => {
componentNavigateObserver.observe(elem, {attributes: false, childList: true, subtree: true});
});

// Initial call to register click events
componentNavigateElementUpdated({ undefined });
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MutationObserver is configured with subtree: true and observes all child list changes within every livewire element. This could trigger componentNavigateElementUpdated very frequently on any DOM change, causing performance issues. Each trigger re-scans all [component:navigate] elements in the document and potentially adds duplicate event listeners. Consider a more targeted approach, such as only observing when livewire elements themselves are added/removed, or using event delegation instead of this observer pattern.

Suggested change
const componentNavigateClick = (event) => {
event.preventDefault();
let component = event.target.getAttribute('data-component');
let params = event.target.getAttribute('data-params');
let livewireElem = event.target.closest("livewire");
livewireElem.setAttribute('data-component', component);
livewireElem.setAttribute('data-params', params ?? '');
renderComponents([{name: component, params: params}]);
};
// Register click event for all elements with attribute "component:navigate"
const componentNavigateElementUpdated = ({el}) => {
if (el == undefined) {
document.querySelectorAll('[component\\:navigate]').forEach(
(elem) => elem.addEventListener('click', componentNavigateClick)
);
return;
}
if (el.hasAttribute('component:navigate')) {
el.addEventListener('click', componentNavigateClick);
return;
}
};
// Trigger event for every livewire update cycle
window.Livewire.hook('morph.updated', componentNavigateElementUpdated);
// Use observer to detect if the component has been replaced
const componentNavigateObserver = new MutationObserver( () => {
componentNavigateElementUpdated({ undefined });
});
document.querySelectorAll('livewire').forEach((elem) => {
componentNavigateObserver.observe(elem, {attributes: false, childList: true, subtree: true});
});
// Initial call to register click events
componentNavigateElementUpdated({ undefined });
const componentNavigateClick = (event, trigger) => {
event.preventDefault();
let component = trigger.getAttribute('data-component');
let params = trigger.getAttribute('data-params');
let livewireElem = trigger.closest("livewire");
if (!livewireElem) {
return;
}
livewireElem.setAttribute('data-component', component);
livewireElem.setAttribute('data-params', params ?? '');
renderComponents([{name: component, params: params}]);
};
// Use event delegation to handle clicks on elements with attribute "component:navigate"
document.addEventListener('click', (event) => {
const trigger = event.target.closest('[component\\:navigate]');
if (!trigger) {
return;
}
componentNavigateClick(event, trigger);
});

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +168
componentNavigateElementUpdated({ undefined });
});
document.querySelectorAll('livewire').forEach((elem) => {
componentNavigateObserver.observe(elem, {attributes: false, childList: true, subtree: true});
});

// Initial call to register click events
componentNavigateElementUpdated({ undefined });
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The object literal syntax { undefined } is incorrect. This should be { el: undefined } to properly pass an object with an el property set to undefined. The current syntax attempts to use shorthand property notation but undefined is a keyword, not a variable name.

Suggested change
componentNavigateElementUpdated({ undefined });
});
document.querySelectorAll('livewire').forEach((elem) => {
componentNavigateObserver.observe(elem, {attributes: false, childList: true, subtree: true});
});
// Initial call to register click events
componentNavigateElementUpdated({ undefined });
componentNavigateElementUpdated({ el: undefined });
});
document.querySelectorAll('livewire').forEach((elem) => {
componentNavigateObserver.observe(elem, {attributes: false, childList: true, subtree: true});
});
// Initial call to register click events
componentNavigateElementUpdated({ el: undefined });

Copilot uses AI. Check for mistakes.
livewireElem.setAttribute('data-component', component);
livewireElem.setAttribute('data-params', params ?? '');

renderComponents([{name: component, params: params}]);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The renderComponents call is missing the key property that's expected based on the existing code pattern. Looking at line 115-119, each component object should have key, name, and params properties. Without the key property, the selector at line 98 (document.querySelector(\[data-component-key="${component}"]`)`) won't be able to find the correct element to update.

Suggested change
renderComponents([{name: component, params: params}]);
renderComponents([{
key: livewireElem.getAttribute('data-component-key'),
name: component,
params: params
}]);

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +137
let component = event.target.getAttribute('data-component');
let params = event.target.getAttribute('data-params');

let livewireElem = event.target.closest("livewire");
livewireElem.setAttribute('data-component', component);
livewireElem.setAttribute('data-params', params ?? '');

Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation for required attributes. If the clicked element doesn't have a data-component attribute, the component variable will be null, which will cause issues when passed to renderComponents and when setting attributes. Add validation to ensure data-component exists before proceeding.

Suggested change
let component = event.target.getAttribute('data-component');
let params = event.target.getAttribute('data-params');
let livewireElem = event.target.closest("livewire");
livewireElem.setAttribute('data-component', component);
livewireElem.setAttribute('data-params', params ?? '');
// Prefer the element the handler is bound to, fall back to the event target
const sourceElement = event.currentTarget || event.target;
if (!sourceElement) {
return;
}
let component = sourceElement.getAttribute('data-component');
let params = sourceElement.getAttribute('data-params');
// Ensure we have a valid component before proceeding
if (!component) {
return;
}
let livewireElem = sourceElement.closest("livewire");
if (livewireElem) {
livewireElem.setAttribute('data-component', component);
livewireElem.setAttribute('data-params', params ?? '');
}

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +134
let component = event.target.getAttribute('data-component');
let params = event.target.getAttribute('data-params');

let livewireElem = event.target.closest("livewire");
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The click event is prevented from its default action (navigation), but the URL is never updated to reflect the new state. When using an anchor tag with component:navigate, users would typically expect the URL to change to match the href attribute, and the browser history to be updated so that back/forward buttons work correctly. Consider using the History API (pushState/replaceState) to update the URL and maintain proper browser history.

Suggested change
let component = event.target.getAttribute('data-component');
let params = event.target.getAttribute('data-params');
let livewireElem = event.target.closest("livewire");
// Use the element that has the event listener (and component:navigate attribute)
const target = event.currentTarget || event.target;
// If the element has an href (e.g. is an <a> tag), update the URL/history
const href = target && target.getAttribute && target.getAttribute('href');
if (href && window.history && typeof window.history.pushState === 'function') {
window.history.pushState({}, '', href);
}
let component = target.getAttribute('data-component');
let params = target.getAttribute('data-params');
let livewireElem = target.closest("livewire");

Copilot uses AI. Check for mistakes.

// Attribute "component:navigate"
document.addEventListener('livewire:initialized', (e) => {
// If a "component:navigate" anker is clicked, load the given component
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misspelling: "anker" should be "anchor".

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +168
componentNavigateElementUpdated({ undefined });
});
document.querySelectorAll('livewire').forEach((elem) => {
componentNavigateObserver.observe(elem, {attributes: false, childList: true, subtree: true});
});

// Initial call to register click events
componentNavigateElementUpdated({ undefined });
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The object literal syntax { undefined } is incorrect. This should be { el: undefined } to properly pass an object with an el property set to undefined. The current syntax attempts to use shorthand property notation but undefined is a keyword, not a variable name.

Suggested change
componentNavigateElementUpdated({ undefined });
});
document.querySelectorAll('livewire').forEach((elem) => {
componentNavigateObserver.observe(elem, {attributes: false, childList: true, subtree: true});
});
// Initial call to register click events
componentNavigateElementUpdated({ undefined });
componentNavigateElementUpdated({ el: undefined });
});
document.querySelectorAll('livewire').forEach((elem) => {
componentNavigateObserver.observe(elem, {attributes: false, childList: true, subtree: true});
});
// Initial call to register click events
componentNavigateElementUpdated({ el: undefined });

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +146
document.querySelectorAll('[component\\:navigate]').forEach(
(elem) => elem.addEventListener('click', componentNavigateClick)
);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event listeners are being added repeatedly without being removed. Each time componentNavigateElementUpdated is called with el == undefined, it adds new click listeners to all [component:navigate] elements (line 144-146). Similarly, the Livewire hook at line 157 and the MutationObserver at line 160-165 will trigger this function multiple times, leading to duplicate event handlers being attached to the same elements. This can cause the click handler to fire multiple times per click and create memory leaks. Consider tracking which elements already have listeners attached, or use event delegation with a single listener on a parent element.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant