8

Consider an AJAX call that writes to a div:

recent_req=$.post('result.php', { d: data }, function(returnData) {
    $('#content').html(returnData);
});

The PHP script at result.php performs some functions that take time, about 5-20 seconds per step. I am using PHP's flush() function to get the info to the browser as soon as each step starts and ends, but how can I get the Javascript to write the data to the #content div as it comes in?

Thanks.

EDIT: To clarify: Assume result.php looks like the following and due to constraints cannot be practically refactored:

<?php

echo "Starting...<br />";
flush();

longOperation();
echo "Done with first long operation.<br />";
flush();

anotherLongOperation();
echo "Done with another long operation.<br />";
flush();

?>

How might the AJAX be structured to call result.php such that the echo statements are appended to the #content div as they come in? Any solution with / without jQuery is welcome. Thanks!

dotancohen
  • 30,064
  • 36
  • 138
  • 197
  • 5
    There is no "as it comes in", the data is available once the request has finished. – adeneo Nov 06 '12 at 12:25
  • A relatively simple workaround is to return data from the first step along with some indication that more data is on it's way. That way you can keep requesting sequentially until all is received. – jensgram Nov 06 '12 at 12:29
  • @adeneo I know I have seen php pages where the content is visible to the user while the script is still are you saying that there is no way to access it when making ajax call? – Joshua Dwire Nov 06 '12 at 12:31
  • @dotancohen The only way I can think of besides splitting the request into multiple request is to make the call using an iframe and show the iframe instead of using ajax. – Joshua Dwire Nov 06 '12 at 12:33
  • @jensgram - Sure, you can make a PHP script that outputs a page, then does something else etc. But you can't make an ajax requests where data is transmitted piece by piece like that, it's sort of one way communication, and it's all or nothing. You'll either have to do subsequent requests or use something like websockets. – adeneo Nov 06 '12 at 12:36
  • @adeneo Indeed. I may not have been entirely clear but I was talking about a sequence of requests, i.e., one request per "piece" (dictated by step or time limit.) – jensgram Nov 06 '12 at 12:40
  • You can't execute a PHP script from multiple ajax requests either, so you would have to split your script into pieces. There are advanced ways to this sort of thing, and things like webworkers, threads and background processes could be worth a look. – adeneo Nov 06 '12 at 12:46
  • Does your async request fail/timeout as it waits for the response at all? Perhaps you could call this synchronously (following full page load), that way your request ought to wait for the full response from your flush. Difficult to say without trying it but it may work. – A.M. D. Nov 06 '12 at 12:29
  • synchronous ajax sort of defeats the purpose ? – adeneo Nov 06 '12 at 12:34
  • That depends on what else the page is doing. If the page has loaded and is not waiting on anything else or performing other updates, its reasonable to make it wait before updating with new content synchronously. – A.M. D. Nov 06 '12 at 12:40
  • Using php's flush you CAN achieve this, and you don't need to display the iframe. You just need to output javascript itself into the iframe as the process continues; you can call any method in the parent window to pass along progress. – Jed Watson Nov 20 '12 at 15:59

7 Answers7

5

There's a technique using an iframe which you could use to achieve this.

Similar to other suggestions involving frames but it doesn't involve sessions or polling or anything, and doesn't need you to display the iframe itself. It also has the benefit of running any code you want at any point in the process, in case you're doing something more sophisticated with your UI than just pushing text to a div (e.g. you could update a progress bar).

Basically, submit the form to a hidden iFrame then flush javascript to that frame, which interacts with functions in the iFrame's parent.

Like this:

HTML:

<form target="results" action="result.php" method="post">
<!-- your form -->
<input type="submit" value="Go" />
</form>

<iframe name="results" id="results" width="0" height="0" />
<div id="progress"></div>

Javascript, in your main page:

function updateProgress(progress) {
  $("#progress").append("<div>" + progress + "</div>");
}

result.php:

<?php

echo "<script language='javascript'>parent.updateProgress('Starting...');</script>";
flush();

longOperation();
echo "<script language='javascript'>parent.updateProgress('Done with first long operation.');</script>";
flush();

anotherLongOperation();
echo "<script language='javascript'>parent.updateProgress('Done with another long operation.');</script>";
flush();

?>
Jed Watson
  • 20,150
  • 3
  • 33
  • 43
4

You cannot 'stream' data using regular ajax calls, for you can't make your user's browser 'listen' to server requests. Your 'success' function will only be called when data's done processing.

There's, though, much discussion on 'Ajax Push' on the internet and apparently HTML5 has websocket objects that can be used to make your user's browser listen to server requests. The syntax definition is not quite stable yet, so you don't want to mess with it, as it may change soon.

What you may want to do is dispatch a request for step1, wait for its return and then dispatch a request for step2. It'll add some overhead to your overall processing time (and will make it much more verbose), but it should work fine if you only have a few big steps. If your steps don't take too much processing, you shouldn't do it (as the communication time will become greater than your 'effective processing time').

EDIT: What you can also do is write the progress on the user's session, for example. That way, you can periodically ping the server with a request for an update on the status. This way, even if you have many small steps, you'll only have to dispatch requests every 10 seconds or so, that being an improvement over dispatching for every step.

Pedro Cordeiro
  • 2,085
  • 1
  • 20
  • 41
  • Thank you. I actually do not want to dispatch multiple requests as the real-world scenario is not a simple as the simplified example that I've posted. – dotancohen Nov 12 '12 at 14:11
  • 2
    What you can also do is write the progress on the user's session, for example. That way, you can periodically ping the server with a request for an update on the status. This way, even if you have many small steps, you'll only have to dispatch requests every 10 seconds or so, that being an improvement over dispatching for every step. – Pedro Cordeiro Nov 12 '12 at 15:53
  • Thanks, that is a possible solution. I'll look into that. – dotancohen Nov 12 '12 at 16:03
3

As an alternative solution, you could submit a hidden form into an iframe, as shown in the following example:

<?php

function output_data($data)  {
    echo str_pad($data, 4096, ' ', STR_PAD_RIGHT) . "\n";
    flush(); 
}

function long_runner() {        
    output_data("");

    output_data(date("H:i:s").' - Starting...<br />');
    sleep(10);

    output_data(date("H:i:s").' - Done with first long operation.<br />');
    sleep(10);

    output_data(date("H:i:s").' - Done with another long operation.<br />');
    return("<script>parent.task_complete()</script>");
}

if (isset($_REQUEST["status"])) {
    die(long_runner());
}

?>
<!DOCTYPE html>
<html>
    <head>
        <title>Write to IFRAME as data streams in</title>
        <style>
        #myform { display: none }
        #frm { width: 50% }
        </style>
        <script language="javascript" type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
        <script>
        function task_complete() {
            alert('Task completed');
        }

        $(document).ready(function() { 
            $('#starter').click(function() {
                $('#myform').submit();
            });
        });
        </script>
    </head>
    <body>
        <form id="myform" method="get" target="frm" action="<?= $_SERVER['SCRIPT_NAME'] ?>">
            <input type="hidden" name="status" value="0">
        </form>
        <a href="#" id="starter">Start</a><br />
        <iframe id="frm" name="frm" frameborder="0"></iframe>
    </body>
</html>
3

Writing a dynamic data stream to a div:

Here goes.. you asked specifically how to dynamically write data streams to a "div". As many have said it is possible to write dynamically to an iframe and we just need to go one step further. Here is a complete solution to your issue, which will bring that data back to your div with a maximum delay of .5 seconds. It can be adapted if you need a more prompt update.

<!DOCTYPE html>
<html>
<head>
    <title>dynamic listener</title>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
    <script type="text/javascript">
    var count;
    $(function(){
        $('#formx').submit(function(){
            setTimeout(function(){ check_div(); }, 500);
            count = 0;
            return true;
        });
    });

    function check_div()
    {
        var $iframetxt = $('#iframex').contents().text();
        var $div = $('#dynamic');
        if( $iframetxt != $div.text() )
        {
            console.log('rewritten!');
            $div.text( $iframetxt );
            setTimeout(function(){ check_div(); }, 500);
            count = 0;
        }
        else
        {
            count++;
            if(count < 40) setTimeout(function(){ check_div(); }, 500);
            else console.log('timed out');
        }       
    }
    </script>
</head>
<body>
    <div>
        Form
        <form id="formx" action="result.php" method="post" target="iframex">
            <input type="submit" />
        </form>
    </div>
    <div id="dynamic"></div>
    <iframe id='iframex' name="iframex" style="display:none" ></iframe>
</body>
</html>

1. On form submit, the streaming data is sent to the iframe.

For this we just set the target attribute in the form tag to the iframe name.

2. check_div() runs every .5 seconds to compare the text of #dynamic div to the text contents of the iframe.

If there is a difference between them, the data is written to the div and the timeout is called again. If there is no difference, the timeout counter increments. If the count is less than 40 (40 x .5 sec = 20 seconds), it calls the timeout again. If not, we assume the stream has completed.

James L.
  • 4,032
  • 1
  • 15
  • 15
  • Thank you James. There is a lot to learn from this post, and I see that you did invest a lot in writing it. However, as much as I enjoyed learning from the post, Jed did post a simpler solution. I do appreciate your help though. – dotancohen Nov 20 '12 at 23:25
  • @dotancohen - His solution was "easier", but DID NOT fit your question. Your claim was that results.php could not be refactored. For his solution to work results.php must be modified! Please be more accurate when you ask questions. – James L. Nov 21 '12 at 09:59
  • I understand what you mean, James. By "refactoring" I was referring to the concept of [restructuring the body of code](http://en.wikipedia.org/wiki/Code_refactoring). Modifications to the output, such as simply wrapping them in javascript, are not usually referred to as refactoring in the communities in which I am familiar. I apologize for not being clear. – dotancohen Nov 21 '12 at 10:19
2

Here is a solution using polling with a session:

HTML:

<!DOCTYPE html>
<html>
<body>
    <div id="content"></div>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
    <script>
    var pollTimeout;
    function pollResult(){
        $.get('poll.php', function(response) {
            // Update #content with partial response
            $('#content').html(response);
            pollTimeout = setTimeout(pollResult, 1000);
        });
    }
    $.post('result.php', function(response) {
        // Result is loaded, stop polling and update content with final response
        clearTimeout(pollTimeout);
        $('#content').html(response);
    });
    // Start polling
    pollResult();
    </script>
</body>

</html>

Result PHP:

<?php

class SemiStream{

    public function __construct(){
        @session_start();
        $_SESSION['semi_stream'] = '';
    }

    public function write($data){
        @session_start();
        $_SESSION['semi_stream'] .= $data;
        // We have to save and close the session to be
        // able to read the contents of it in poll.php
        session_write_close();
    }

    public function close(){
        echo $_SESSION['semi_stream'];
        unset($_SESSION['semi_stream']);
    }
}

$stream = new SemiStream();
$stream->write("Starting...<br />");

sleep(3);

$stream->write("Done with first long operation.<br />");

sleep(3);

$stream->write("Done with another long operation.<br />");
$stream->close();

echo 'Done.';

Poll PHP:

<?php
session_start();
echo $_SESSION['semi_stream'];

This works, without the use of PHP's output buffering.

Koen.
  • 25,449
  • 7
  • 83
  • 78
  • Thanks, Koen. Though there is a lot of good information here to learn from, Jed did post a more straightforward solution. – dotancohen Nov 20 '12 at 23:42
0

Check out the Pusher service, seems like it could do exactly what you want: http://pusher.com/

Derek
  • 4,575
  • 3
  • 22
  • 36
0

Probably, the question is about how to implement Push technology in your app. I would suggest you to look this question which has great answer with example

Community
  • 1
  • 1
pinepain
  • 12,453
  • 3
  • 60
  • 65