Skip to content

Instantly share code, notes, and snippets.

@simonw
Created November 15, 2024 03:22
Show Gist options
  • Save simonw/f3f0089028aabfca1e0f94f937fd8641 to your computer and use it in GitHub Desktop.
Save simonw/f3f0089028aabfca1e0f94f937fd8641 to your computer and use it in GitHub Desktop.

Sandboxed API Explorer Tool

human (Nov 14, 2024, 05:53 PM)

Build me an HTML page that adds an iframe with sandbox=allow-scripts on it and then injects code into that iframe that implements a page with a simple API explorer tool - it provides a URL bar and a Submit button and a big pre tag that gets populated with pretty-printed JSON after the API URL is submitted

Here's the catch: because it is in an iframe sandbox it cannot make HTTP calls itself. Instead, it should use postMessage to its parent window with a JSON blob describing the HTTP call it needs to make - the method and the URL - and the parent page should catch that message, make the API call using fetch() and then send the resulting JSON back to the child frame

assistant (Nov 14, 2024, 05:53 PM)

I'll help you build this API explorer tool with a sandboxed iframe setup. This is a good candidate for creating an HTML artifact.

create API Explorer with Sandboxed iframe

<!DOCTYPE html>
<html>
<head>
    <title>API Explorer</title>
    <style>
        body {
            font-family: system-ui, -apple-system, sans-serif;
            margin: 0;
            padding: 20px;
            background: #f0f0f0;
        }
        iframe {
            width: 100%;
            height: 600px;
            border: 1px solid #ccc;
            border-radius: 4px;
            background: white;
        }
    </style>
</head>
<body>
    <iframe id="explorerFrame" sandbox="allow-scripts"></iframe>

    <script>
        // Create the HTML content for the iframe
        const iframeContent = `
            <!DOCTYPE html>
            <html>
            <head>
                <style>
                    body {
                        font-family: system-ui, -apple-system, sans-serif;
                        margin: 0;
                        padding: 20px;
                    }
                    .container {
                        max-width: 800px;
                        margin: 0 auto;
                    }
                    .input-group {
                        display: flex;
                        gap: 8px;
                        margin-bottom: 20px;
                    }
                    input[type="text"] {
                        flex: 1;
                        padding: 8px;
                        border: 1px solid #ccc;
                        border-radius: 4px;
                        font-size: 16px;
                    }
                    button {
                        padding: 8px 16px;
                        background: #0066cc;
                        color: white;
                        border: none;
                        border-radius: 4px;
                        cursor: pointer;
                        font-size: 16px;
                    }
                    button:hover {
                        background: #0052a3;
                    }
                    pre {
                        background: #f5f5f5;
                        padding: 20px;
                        border-radius: 4px;
                        overflow-x: auto;
                        white-space: pre-wrap;
                        word-wrap: break-word;
                    }
                    .error {
                        color: #cc0000;
                    }
                </style>
            </head>
            <body>
                <div class="container">
                    <h1>API Explorer</h1>
                    <div class="input-group">
                        <input type="text" id="urlInput" placeholder="Enter API URL" value="https://api.example.com/data">
                        <button onclick="makeRequest()">Submit</button>
                    </div>
                    <pre id="output">// Response will appear here</pre>
                </div>

                <script>
                    function makeRequest() {
                        const url = document.getElementById('urlInput').value;
                        const output = document.getElementById('output');
                        
                        // Send message to parent window
                        window.parent.postMessage({
                            type: 'api-request',
                            url: url,
                            method: 'GET'
                        }, '*');

                        output.textContent = 'Loading...';
                    }

                    // Listen for response from parent
                    window.addEventListener('message', function(event) {
                        const output = document.getElementById('output');
                        
                        if (event.data.type === 'api-response') {
                            if (event.data.error) {
                                output.textContent = 'Error: ' + event.data.error;
                                output.classList.add('error');
                            } else {
                                output.textContent = JSON.stringify(event.data.response, null, 2);
                                output.classList.remove('error');
                            }
                        }
                    });
                </script>
            </body>
            </html>
        `;

        // Set up the iframe with the content
        const iframe = document.getElementById('explorerFrame');
        iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(iframeContent);

        // Listen for messages from the iframe
        window.addEventListener('message', async function(event) {
            if (event.data.type === 'api-request') {
                try {
                    const response = await fetch(event.data.url, {
                        method: event.data.method
                    });
                    
                    const data = await response.json();
                    
                    // Send response back to iframe
                    iframe.contentWindow.postMessage({
                        type: 'api-response',
                        response: data
                    }, '*');
                } catch (error) {
                    // Send error back to iframe
                    iframe.contentWindow.postMessage({
                        type: 'api-response',
                        error: error.message
                    }, '*');
                }
            }
        });
    </script>
</body>
</html>

I've created an HTML page that implements the API explorer tool with the following features:

  1. A parent page containing a sandboxed iframe with allow-scripts
  2. The iframe contains:
    • A URL input field
    • A Submit button
    • A pre-formatted output area for displaying JSON responses
    • Clean, modern styling
  3. Communication flow:
    • When the Submit button is clicked, the iframe sends a postMessage to the parent with the URL and method
    • The parent catches this message, makes the actual fetch() call
    • The parent sends the response back to the iframe using postMessage
    • The iframe displays the pretty-printed JSON response

The solution includes error handling and a nice user interface. To use it:

  1. Enter an API URL in the input field
  2. Click Submit
  3. The response will appear in the formatted output area below

If there's an error (like an invalid URL or failed request), it will be displayed in red.

Would you like me to explain any part of the implementation in more detail or make any adjustments to the functionality?

human (Nov 14, 2024, 05:56 PM)

I edited it to be like this.Now add a second bit of clever postMessage JavaScript so the child window can tell the parent window what size the iframe should be when it resizes

paste.txt

<!DOCTYPE html>
<html>
<head>
    <title>API Explorer</title>
    <style>
        body {
            font-family: system-ui, -apple-system, sans-serif;
            margin: 0;
            padding: 20px;
            background: #f0f0f0;
        }
        iframe {
            width: 100%;
            height: 600px;
            border: 1px solid #ccc;
            border-radius: 4px;
            background: white;
        }
    </style>
</head>
<body>
    <iframe id="explorerFrame" sandbox="allow-scripts"></iframe>

    <script>
        // Create the HTML content for the iframe
        const iframeContent = `
            <!DOCTYPE html>
            <html>
            <head>
                <style>
                    body {
                        font-family: system-ui, -apple-system, sans-serif;
                        margin: 0;
                        padding: 20px;
                    }
                    .container {
                        max-width: 800px;
                        margin: 0 auto;
                    }
                    .input-group {
                        display: flex;
                        gap: 8px;
                        margin-bottom: 20px;
                    }
                    input[type="text"] {
                        flex: 1;
                        padding: 8px;
                        border: 1px solid #ccc;
                        border-radius: 4px;
                        font-size: 16px;
                    }
                    button {
                        padding: 8px 16px;
                        background: #0066cc;
                        color: white;
                        border: none;
                        border-radius: 4px;
                        cursor: pointer;
                        font-size: 16px;
                    }
                    button:hover {
                        background: #0052a3;
                    }
                    pre {
                        background: #f5f5f5;
                        padding: 20px;
                        border-radius: 4px;
                        overflow-x: auto;
                        white-space: pre-wrap;
                        word-wrap: break-word;
                    }
                    .error {
                        color: #cc0000;
                    }
                </style>
            </head>
            <body>
                <div class="container">
                    <h1>API Explorer</h1>
                    <div class="input-group">
                        <input type="text" id="urlInput" placeholder="Enter API URL" value="https://api.example.com/data">
                        <button onclick="makeRequest()">Submit</button>
                    </div>
                    <pre id="output">// Response will appear here</pre>
                </div>

                <script>
                    function makeRequest() {
                        const url = document.getElementById('urlInput').value;
                        const output = document.getElementById('output');
                        
                        // Send message to parent window
                        window.parent.postMessage({
                            type: 'api-request',
                            url: url,
                            method: 'GET'
                        }, '*');

                        output.textContent = 'Loading...';
                    }

                    // Listen for response from parent
                    window.addEventListener('message', function(event) {
                        const output = document.getElementById('output');
                        
                        if (event.data.type === 'api-response') {
                            if (event.data.error) {
                                output.textContent = 'Error: ' + event.data.error;
                                output.classList.add('error');
                            } else {
                                output.textContent = JSON.stringify(event.data.response, null, 2);
                                output.classList.remove('error');
                            }
                        }
                    });
                SCUB
            </body>
            </html>
        `.replace('SCUB', '</' + 'script>');

        // Set up the iframe with the content
        const iframe = document.getElementById('explorerFrame');
        iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(iframeContent);

        // Listen for messages from the iframe
        window.addEventListener('message', async function(event) {
            if (event.data.type === 'api-request') {
                try {
                    const response = await fetch(event.data.url, {
                        method: event.data.method
                    });
                    
                    const data = await response.json();
                    
                    // Send response back to iframe
                    iframe.contentWindow.postMessage({
                        type: 'api-response',
                        response: data
                    }, '*');
                } catch (error) {
                    // Send error back to iframe
                    iframe.contentWindow.postMessage({
                        type: 'api-response',
                        error: error.message
                    }, '*');
                }
            }
        });
    </script>
</body>
</html>



assistant (Nov 14, 2024, 05:56 PM)

I'll modify the code to add dynamic iframe resizing through postMessage communication. The child frame will measure its content and notify the parent whenever the size changes.

rewrite Untitled

<!DOCTYPE html>
<html>
<head>
    <title>API Explorer</title>
    <style>
        body {
            font-family: system-ui, -apple-system, sans-serif;
            margin: 0;
            padding: 20px;
            background: #f0f0f0;
        }
        iframe {
            width: 100%;
            border: 1px solid #ccc;
            border-radius: 4px;
            background: white;
            transition: height 0.2s ease;
        }
    </style>
</head>
<body>
    <iframe id="explorerFrame" sandbox="allow-scripts"></iframe>

    <script>
        // Create the HTML content for the iframe
        const iframeContent = `
            <!DOCTYPE html>
            <html>
            <head>
                <style>
                    body {
                        font-family: system-ui, -apple-system, sans-serif;
                        margin: 0;
                        padding: 20px;
                    }
                    .container {
                        max-width: 800px;
                        margin: 0 auto;
                    }
                    .input-group {
                        display: flex;
                        gap: 8px;
                        margin-bottom: 20px;
                    }
                    input[type="text"] {
                        flex: 1;
                        padding: 8px;
                        border: 1px solid #ccc;
                        border-radius: 4px;
                        font-size: 16px;
                    }
                    button {
                        padding: 8px 16px;
                        background: #0066cc;
                        color: white;
                        border: none;
                        border-radius: 4px;
                        cursor: pointer;
                        font-size: 16px;
                    }
                    button:hover {
                        background: #0052a3;
                    }
                    pre {
                        background: #f5f5f5;
                        padding: 20px;
                        border-radius: 4px;
                        overflow-x: auto;
                        white-space: pre-wrap;
                        word-wrap: break-word;
                    }
                    .error {
                        color: #cc0000;
                    }
                </style>
            </head>
            <body>
                <div class="container">
                    <h1>API Explorer</h1>
                    <div class="input-group">
                        <input type="text" id="urlInput" placeholder="Enter API URL" value="https://api.example.com/data">
                        <button onclick="makeRequest()">Submit</button>
                    </div>
                    <pre id="output">// Response will appear here</pre>
                </div>

                <script>
                    function updateParentHeight() {
                        const height = document.documentElement.scrollHeight;
                        window.parent.postMessage({
                            type: 'resize',
                            height: height
                        }, '*');
                    }

                    // Call initially and set up resize observer
                    updateParentHeight();
                    const resizeObserver = new ResizeObserver(() => {
                        updateParentHeight();
                    });
                    resizeObserver.observe(document.body);

                    function makeRequest() {
                        const url = document.getElementById('urlInput').value;
                        const output = document.getElementById('output');
                        
                        // Send message to parent window
                        window.parent.postMessage({
                            type: 'api-request',
                            url: url,
                            method: 'GET'
                        }, '*');

                        output.textContent = 'Loading...';
                    }

                    // Listen for response from parent
                    window.addEventListener('message', function(event) {
                        const output = document.getElementById('output');
                        
                        if (event.data.type === 'api-response') {
                            if (event.data.error) {
                                output.textContent = 'Error: ' + event.data.error;
                                output.classList.add('error');
                            } else {
                                output.textContent = JSON.stringify(event.data.response, null, 2);
                                output.classList.remove('error');
                            }
                            // Content has changed, update height
                            updateParentHeight();
                        }
                    });
                SCUB
            </body>
            </html>
        `.replace('SCUB', '</' + 'script>');

        // Set up the iframe with the content
        const iframe = document.getElementById('explorerFrame');
        iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(iframeContent);

        // Listen for messages from the iframe
        window.addEventListener('message', async function(event) {
            if (event.data.type === 'api-request') {
                try {
                    const response = await fetch(event.data.url, {
                        method: event.data.method
                    });
                    
                    const data = await response.json();
                    
                    // Send response back to iframe
                    iframe.contentWindow.postMessage({
                        type: 'api-response',
                        response: data
                    }, '*');
                } catch (error) {
                    // Send error back to iframe
                    iframe.contentWindow.postMessage({
                        type: 'api-response',
                        error: error.message
                    }, '*');
                }
            } else if (event.data.type === 'resize') {
                // Update iframe height when child requests it
                iframe.style.height = event.data.height + 'px';
            }
        });
    </script>
</body>
</html>

I've made the following changes to implement dynamic iframe resizing:

  1. In the parent window:

    • Removed the fixed height from the iframe CSS
    • Added a smooth transition for height changes
    • Added handling for 'resize' type messages from the child
  2. In the child window:

    • Added a updateParentHeight() function that measures the document height and sends it to the parent
    • Added a ResizeObserver to detect content size changes
    • Called updateParentHeight() on initial load and after API responses
    • The height is calculated using document.documentElement.scrollHeight

The iframe will now automatically adjust its height when:

  • The page initially loads
  • The content changes after an API response
  • Any dynamic content causes the page to resize

The ResizeObserver ensures we catch all size changes, even ones we didn't explicitly trigger. The transition CSS property makes the height changes smooth rather than jarring.

human (Nov 14, 2024, 05:58 PM)

I'm seeing this warning

TypeError: URL constructor: null is not a valid URL.

assistant (Nov 14, 2024, 05:58 PM)

That warning is occurring because we're not validating the URL before making the fetch request. Let's add URL validation in both the child iframe (for immediate feedback) and the parent (as a safety check).

rewrite Untitled

<!DOCTYPE html>
<html>
<head>
    <title>API Explorer</title>
    <style>
        body {
            font-family: system-ui, -apple-system, sans-serif;
            margin: 0;
            padding: 20px;
            background: #f0f0f0;
        }
        iframe {
            width: 100%;
            border: 1px solid #ccc;
            border-radius: 4px;
            background: white;
            transition: height 0.2s ease;
        }
    </style>
</head>
<body>
    <iframe id="explorerFrame" sandbox="allow-scripts"></iframe>

    <script>
        // Create the HTML content for the iframe
        const iframeContent = `
            <!DOCTYPE html>
            <html>
            <head>
                <style>
                    body {
                        font-family: system-ui, -apple-system, sans-serif;
                        margin: 0;
                        padding: 20px;
                    }
                    .container {
                        max-width: 800px;
                        margin: 0 auto;
                    }
                    .input-group {
                        display: flex;
                        gap: 8px;
                        margin-bottom: 20px;
                    }
                    input[type="text"] {
                        flex: 1;
                        padding: 8px;
                        border: 1px solid #ccc;
                        border-radius: 4px;
                        font-size: 16px;
                    }
                    input[type="text"].error {
                        border-color: #cc0000;
                    }
                    button {
                        padding: 8px 16px;
                        background: #0066cc;
                        color: white;
                        border: none;
                        border-radius: 4px;
                        cursor: pointer;
                        font-size: 16px;
                    }
                    button:hover {
                        background: #0052a3;
                    }
                    button:disabled {
                        background: #cccccc;
                        cursor: not-allowed;
                    }
                    pre {
                        background: #f5f5f5;
                        padding: 20px;
                        border-radius: 4px;
                        overflow-x: auto;
                        white-space: pre-wrap;
                        word-wrap: break-word;
                    }
                    .error {
                        color: #cc0000;
                    }
                </style>
            </head>
            <body>
                <div class="container">
                    <h1>API Explorer</h1>
                    <div class="input-group">
                        <input 
                            type="text" 
                            id="urlInput" 
                            placeholder="Enter API URL" 
                            value="https://api.example.com/data"
                            oninput="validateUrl(this.value)"
                        >
                        <button id="submitBtn" onclick="makeRequest()">Submit</button>
                    </div>
                    <pre id="output">// Response will appear here</pre>
                </div>

                <script>
                    // Initialize UI elements
                    const urlInput = document.getElementById('urlInput');
                    const submitBtn = document.getElementById('submitBtn');
                    const output = document.getElementById('output');

                    function isValidUrl(string) {
                        try {
                            new URL(string);
                            return true;
                        } catch (_) {
                            return false;
                        }
                    }

                    function validateUrl(value) {
                        const valid = isValidUrl(value);
                        urlInput.classList.toggle('error', !valid);
                        submitBtn.disabled = !valid;
                        if (!valid && value !== '') {
                            output.textContent = 'Please enter a valid URL (must start with http:// or https://)';
                            output.classList.add('error');
                        } else {
                            output.textContent = '// Response will appear here';
                            output.classList.remove('error');
                        }
                        return valid;
                    }

                    function updateParentHeight() {
                        const height = document.documentElement.scrollHeight;
                        window.parent.postMessage({
                            type: 'resize',
                            height: height
                        }, '*');
                    }

                    // Call initially and set up resize observer
                    updateParentHeight();
                    const resizeObserver = new ResizeObserver(() => {
                        updateParentHeight();
                    });
                    resizeObserver.observe(document.body);

                    function makeRequest() {
                        const url = urlInput.value;
                        
                        if (!validateUrl(url)) {
                            return;
                        }
                        
                        // Send message to parent window
                        window.parent.postMessage({
                            type: 'api-request',
                            url: url,
                            method: 'GET'
                        }, '*');

                        output.textContent = 'Loading...';
                        output.classList.remove('error');
                    }

                    // Initial URL validation
                    validateUrl(urlInput.value);

                    // Listen for response from parent
                    window.addEventListener('message', function(event) {
                        if (event.data.type === 'api-response') {
                            if (event.data.error) {
                                output.textContent = 'Error: ' + event.data.error;
                                output.classList.add('error');
                            } else {
                                output.textContent = JSON.stringify(event.data.response, null, 2);
                                output.classList.remove('error');
                            }
                            // Content has changed, update height
                            updateParentHeight();
                        }
                    });
                SCUB
            </body>
            </html>
        `.replace('SCUB', '</' + 'script>');

        // Set up the iframe with the content
        const iframe = document.getElementById('explorerFrame');
        iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(iframeContent);

        // Listen for messages from the iframe
        window.addEventListener('message', async function(event) {
            if (event.data.type === 'api-request') {
                try {
                    // Double-check URL validity in parent as well
                    const url = new URL(event.data.url);
                    
                    const response = await fetch(url.href, {
                        method: event.data.method
                    });
                    
                    const data = await response.json();
                    
                    // Send response back to iframe
                    iframe.contentWindow.postMessage({
                        type: 'api-response',
                        response: data
                    }, '*');
                } catch (error) {
                    // Send error back to iframe
                    iframe.contentWindow.postMessage({
                        type: 'api-response',
                        error: error.message
                    }, '*');
                }
            } else if (event.data.type === 'resize') {
                // Update iframe height when child requests it
                iframe.style.height = event.data.height + 'px';
            }
        });
    </script>
</body>
</html>

I've made several improvements to handle URL validation:

  1. Added a validateUrl() function in the child iframe that:

    • Uses the URL constructor to validate URLs
    • Shows visual feedback when the URL is invalid
    • Disables the submit button for invalid URLs
    • Shows an error message explaining the URL must start with http:// or https://
  2. Added real-time validation:

    • Validates on input change
    • Validates before making requests
    • Shows red border on invalid URL input
    • Disables submit button when URL is invalid
  3. Added double-check validation in the parent window:

    • Creates a new URL object before fetching
    • Uses the normalized url.href for the fetch call
    • Properly catches and reports URL-related errors
  4. Improved the UI:

    • Added disabled state styling for the submit button
    • Added error state styling for the input field
    • Added helpful error messages
    • Maintains all previous functionality for resizing and API calls

Now the URL validation happens in real-time and provides good visual feedback to the user, and you shouldn't see that TypeError anymore.

human (Nov 14, 2024, 05:59 PM)

the iframe should get shorter if a new response comes back that is shorter

assistant (Nov 14, 2024, 05:59 PM)

Yes, good catch! The iframe needs to shrink when new content is smaller. The ResizeObserver should handle this, but let's make absolutely sure by improving the height calculation.

update api-explorer

Find this:

                    function updateParentHeight() {
                        const height = document.documentElement.scrollHeight;
                        window.parent.postMessage({
                            type: 'resize',
                            height: height
                        }, '*');
                    }

Replace with this:

                    function updateParentHeight() {
                        // Reset any minimum height that might prevent shrinking
                        document.body.style.minHeight = '0px';
                        
                        // Get the actual content height
                        const height = Math.max(
                            document.body.scrollHeight,
                            document.body.offsetHeight,
                            document.documentElement.scrollHeight,
                            document.documentElement.offsetHeight
                        );
                        
                        window.parent.postMessage({
                            type: 'resize',
                            height: height
                        }, '*');
                    }

I've improved the height calculation to:

  1. Reset any minimum height that might prevent shrinking
  2. Take the maximum of various height measurements to ensure we capture the true content height
  3. This will now properly shrink when new content is shorter than previous content

Let's also make sure the parent iframe handles the resize smoothly:

update api-explorer

Find this:

            transition: height 0.2s ease;

Replace with this:

            transition: height 0.2s ease-out;
            min-height: 200px; /* Ensure a minimum height */

I've made these additional improvements:

  1. Added a minimum height to the iframe so it never collapses completely
  2. Changed the transition timing function to ease-out for smoother shrinking
  3. The iframe will now smoothly animate to both larger and smaller sizes while never getting too small

These changes should ensure the iframe properly resizes both up and down based on its content, with smooth transitions in both directions.

human (Nov 14, 2024, 06:01 PM)

it's not resizing well - add a bunch of console.log statements

Set default URL to https://datasette.io/content/stats.json

Use a form and a submit button such that hitting enter submits it

assistant (Nov 14, 2024, 06:01 PM)

I'll add detailed logging and make those improvements to help debug the resizing issue.

rewrite Untitled

<!DOCTYPE html>
<html>
<head>
    <title>API Explorer</title>
    <style>
        body {
            font-family: system-ui, -apple-system, sans-serif;
            margin: 0;
            padding: 20px;
            background: #f0f0f0;
        }
        iframe {
            width: 100%;
            border: 1px solid #ccc;
            border-radius: 4px;
            background: white;
            transition: height 0.2s ease-out;
            min-height: 200px;
        }
    </style>
</head>
<body>
    <iframe id="explorerFrame" sandbox="allow-scripts"></iframe>

    <script>
        // Create the HTML content for the iframe
        const iframeContent = `
            <!DOCTYPE html>
            <html>
            <head>
                <style>
                    body {
                        font-family: system-ui, -apple-system, sans-serif;
                        margin: 0;
                        padding: 20px;
                    }
                    .container {
                        max-width: 800px;
                        margin: 0 auto;
                    }
                    form {
                        margin-bottom: 20px;
                    }
                    .input-group {
                        display: flex;
                        gap: 8px;
                    }
                    input[type="text"] {
                        flex: 1;
                        padding: 8px;
                        border: 1px solid #ccc;
                        border-radius: 4px;
                        font-size: 16px;
                    }
                    input[type="text"].error {
                        border-color: #cc0000;
                    }
                    button {
                        padding: 8px 16px;
                        background: #0066cc;
                        color: white;
                        border: none;
                        border-radius: 4px;
                        cursor: pointer;
                        font-size: 16px;
                    }
                    button:hover {
                        background: #0052a3;
                    }
                    button:disabled {
                        background: #cccccc;
                        cursor: not-allowed;
                    }
                    pre {
                        background: #f5f5f5;
                        padding: 20px;
                        border-radius: 4px;
                        overflow-x: auto;
                        white-space: pre-wrap;
                        word-wrap: break-word;
                        margin: 0;
                    }
                    .error {
                        color: #cc0000;
                    }
                </style>
            </head>
            <body>
                <div class="container">
                    <h1>API Explorer</h1>
                    <form id="apiForm" onsubmit="makeRequest(event)">
                        <div class="input-group">
                            <input 
                                type="text" 
                                id="urlInput" 
                                placeholder="Enter API URL" 
                                value="https://datasette.io/content/stats.json"
                                oninput="validateUrl(this.value)"
                            >
                            <button type="submit" id="submitBtn">Submit</button>
                        </div>
                    </form>
                    <pre id="output">// Response will appear here</pre>
                </div>

                <script>
                    // Initialize UI elements
                    const urlInput = document.getElementById('urlInput');
                    const submitBtn = document.getElementById('submitBtn');
                    const output = document.getElementById('output');
                    let lastHeight = 0;

                    function isValidUrl(string) {
                        try {
                            new URL(string);
                            return true;
                        } catch (_) {
                            return false;
                        }
                    }

                    function validateUrl(value) {
                        console.log('Validating URL:', value);
                        const valid = isValidUrl(value);
                        urlInput.classList.toggle('error', !valid);
                        submitBtn.disabled = !valid;
                        if (!valid && value !== '') {
                            output.textContent = 'Please enter a valid URL (must start with http:// or https://)';
                            output.classList.add('error');
                        } else {
                            output.textContent = '// Response will appear here';
                            output.classList.remove('error');
                        }
                        updateParentHeight();
                        return valid;
                    }

                    function updateParentHeight() {
                        // Reset any minimum height
                        document.body.style.minHeight = '0px';
                        
                        // Get various height measurements
                        const bodyScroll = document.body.scrollHeight;
                        const bodyOffset = document.body.offsetHeight;
                        const htmlScroll = document.documentElement.scrollHeight;
                        const htmlOffset = document.documentElement.offsetHeight;
                        
                        // Get the maximum height
                        const height = Math.max(bodyScroll, bodyOffset, htmlScroll, htmlOffset);
                        
                        console.log('Height measurements:', {
                            bodyScroll,
                            bodyOffset,
                            htmlScroll,
                            htmlOffset,
                            finalHeight: height,
                            lastHeight
                        });

                        if (height !== lastHeight) {
                            console.log('Sending new height to parent:', height);
                            window.parent.postMessage({
                                type: 'resize',
                                height: height
                            }, '*');
                            lastHeight = height;
                        }
                    }

                    // Call initially and set up resize observer
                    updateParentHeight();
                    const resizeObserver = new ResizeObserver((entries) => {
                        console.log('ResizeObserver triggered:', entries[0].contentRect);
                        updateParentHeight();
                    });
                    resizeObserver.observe(document.body);

                    function makeRequest(event) {
                        if (event) {
                            event.preventDefault();
                        }
                        
                        const url = urlInput.value;
                        console.log('Making request to:', url);
                        
                        if (!validateUrl(url)) {
                            return;
                        }
                        
                        // Send message to parent window
                        window.parent.postMessage({
                            type: 'api-request',
                            url: url,
                            method: 'GET'
                        }, '*');

                        output.textContent = 'Loading...';
                        output.classList.remove('error');
                        updateParentHeight();
                    }

                    // Initial URL validation
                    validateUrl(urlInput.value);

                    // Listen for response from parent
                    window.addEventListener('message', function(event) {
                        console.log('Received message from parent:', event.data);
                        
                        if (event.data.type === 'api-response') {
                            if (event.data.error) {
                                output.textContent = 'Error: ' + event.data.error;
                                output.classList.add('error');
                            } else {
                                const formatted = JSON.stringify(event.data.response, null, 2);
                                console.log('Setting response, length:', formatted.length);
                                output.textContent = formatted;
                                output.classList.remove('error');
                            }
                            // Content has changed, update height
                            updateParentHeight();
                        }
                    });
                SCUB
            </body>
            </html>
        `.replace('SCUB', '</' + 'script>');

        // Set up the iframe with the content
        const iframe = document.getElementById('explorerFrame');
        iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(iframeContent);

        // Listen for messages from the iframe
        window.addEventListener('message', async function(event) {
            console.log('Parent received message:', event.data);
            
            if (event.data.type === 'api-request') {
                try {
                    // Double-check URL validity in parent as well
                    const url = new URL(event.data.url);
                    console.log('Parent making fetch request to:', url.href);
                    
                    const response = await fetch(url.href, {
                        method: event.data.method
                    });
                    
                    const data = await response.json();
                    console.log('Parent received response, size:', JSON.stringify(data).length);
                    
                    // Send response back to iframe
                    iframe.contentWindow.postMessage({
                        type: 'api-response',
                        response: data
                    }, '*');
                } catch (error) {
                    console.error('Parent encountered error:', error);
                    // Send error back to iframe
                    iframe.contentWindow.postMessage({
                        type: 'api-response',
                        error: error.message
                    }, '*');
                }
            } else if (event.data.type === 'resize') {
                console.log('Parent setting iframe height:', event.data.height);
                iframe.style.height = event.data.height + 'px';
            }
        });
    </script>
</body>
</html>

I've made several improvements:

  1. Added extensive console.log statements throughout to track:

    • Height calculations and changes
    • Message passing between parent and child
    • Response sizes
    • ResizeObserver triggers
    • URL validation
    • Error states
  2. Changed to a proper form with onsubmit handler so Enter key works

  3. Changed default URL to https://datasette.io/content/stats.json

  4. Added tracking of lastHeight to avoid unnecessary resize messages

  5. Improved height calculation logging to show all measurements

  6. Added margin: 0 to the pre element to avoid extra space

  7. Added more detailed timing and size logging for responses

The console logs should help us identify why the resizing isn't working properly. When you run it now, you should see detailed logs about:

  • Initial size calculations
  • Size changes when content updates
  • All height measurements being considered
  • When resize messages are sent and received

This should help pinpoint where the resizing logic might be failing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment