Hello, I am trying to add custom JS to the appointment booking widget.
I want us JS to change the Email field to an Address field that has validation for just text and not an email format.
When I use “document.getElementById(“email”)” it returns an error saying it could not find the element. The issue is that the code is executed before the widget is rendered.
Is there a way for me to wait until the user opens the widget before trying to run the custom JS?
Thank you!
2 Likes
Max
March 10, 2025, 5:43pm
3
Hi there, @user2661
At the moment, unfortunately, it’s impossible to transform the Email into the Address field even with the Custom JS code.
However, the good news is that we’re currently working on the custom field implementation. Please upvote this request to receive a notification once the feature is released - Add custom fields to the booking form
1 Like
Hello, thanks for your reply.
I will upvote the feature request, but in the meantime is there a way I can run code when the widget actually loads after the customer presses the “Book Now” button?
That way I can just modify the display texts to show “Address” instead of “Notes” and use custom JS to make the “Notes” field always visible.
Thanks!
2 Likes
Max
March 10, 2025, 6:30pm
5
Please try to use this code in the Custom JS section on the Settings tab of your widget’s settings and let me know if it worked:
function listener(selector, callback) {
const firstTarget = document.querySelector(selector);
if (firstTarget) {
return callback(firstTarget);
}
const observer = new MutationObserver((_, observer) => {
const targetNode = document.querySelector(selector);
if (targetNode) {
observer.disconnect();
callback(targetNode);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function disabledButton(button) {
button.style.pointerEvents = 'none';
button.style.opacity = 0.3;
button.disabled = true;
}
function activatedButton(button) {
button.style.pointerEvents = 'auto';
button.style.opacity = 1;
button.disabled = false;
}
listener(
'[class*="layout__Container-sc"] [class*="content__Container-sc"]',
(container) => {
const mutationObserver = new MutationObserver((mutations) => {
const isAddedForm = mutations.some(({ addedNodes }) =>
Array.from(addedNodes).some((node) =>
node.className?.includes('form-layout__Container')
)
);
if (isAddedForm) {
const formLayout = container.querySelector(
'[class*="form-layout__Container-sc"]'
);
const messageButton = formLayout.querySelector(
'[class*="link-button__Container-sc"]'
);
const submitButton = formLayout.querySelector(
'[class*="ButtonBase__ButtonContainer-sc"]'
);
disabledButton(submitButton);
if (messageButton) {
requestAnimationFrame(() => {
messageButton.click();
});
setTimeout(() => {
const label = container.querySelector(
'[for="notes"][class*="TextControlBase__TextControlBaseLabel-sc"]'
);
if (label) {
label.textContent = 'Address';
}
const textArea = formLayout.querySelector('textarea');
textArea.addEventListener('input', ({ target }) => {
if (target.value.trim()) {
activatedButton(submitButton);
} else {
disabledButton(submitButton);
}
});
}, 100);
}
}
});
mutationObserver.observe(container, {
childList: true,
subtree: true
});
}
);
2 Likes
Thanks again for the help! I tried out the code and it works for what I need!
I really appreciate your help @Max
One thing I noticed is that if you close or click outside of the appointment booking window and then click to book again, your info is still preserved but the “Notes” field goes back to its original state and is hidden. I’m not asking for a fix as it’s too minor for me, I just wanted to make a note of it.
Thank you once again!
3 Likes
Max
March 11, 2025, 12:11pm
7
Thank you so much for your note!
Our devs will try to adjust the code and I’ll share the revised version here
Max
March 12, 2025, 11:23am
8
Hi @user2661
Here is the adjusted code that should work great
function listener(selector, callback) {
const firstTarget = document.querySelector(selector);
if (firstTarget) {
return callback(firstTarget);
}
const observer = new MutationObserver(() => {
const targetNode = document.querySelector(selector);
if (targetNode) {
callback(targetNode);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function disabledButton(button) {
button.style.pointerEvents = 'none';
button.style.opacity = 0.3;
button.disabled = true;
}
function activatedButton(button) {
button.style.pointerEvents = 'auto';
button.style.opacity = 1;
button.disabled = false;
}
listener(
'[class*="layout__Container-sc"] [class*="content__Container-sc"]',
(container) => {
const callback = (mutations) => {
const isAddedForm = mutations.some(({ addedNodes }) =>
Array.from(addedNodes).some((node) =>
node.className?.includes('form-layout__Container')
)
);
if (isAddedForm) {
const formLayout = container.querySelector(
'[class*="form-layout__Container-sc"]'
);
const messageButton = formLayout.querySelector(
'[class*="link-button__Container-sc"]'
);
const submitButton = formLayout.querySelector(
'[class*="ButtonBase__ButtonContainer-sc"]'
);
disabledButton(submitButton);
if (messageButton) {
requestAnimationFrame(() => {
messageButton.click();
});
setTimeout(() => {
const label = container.querySelector(
'[for="notes"][class*="TextControlBase__TextControlBaseLabel-sc"]'
);
if (label) {
label.textContent = 'Address';
}
const textArea = formLayout.querySelector('textarea');
textArea.addEventListener('input', ({ target }) => {
if (target.value.trim()) {
activatedButton(submitButton);
} else {
disabledButton(submitButton);
}
});
}, 100);
}
}
};
callback([{ addedNodes: container.childNodes }]);
const mutationObserver = new MutationObserver(callback);
mutationObserver.observe(container, {
childList: true,
subtree: true
});
}
);
1 Like