Jump to content

How to work with AJAX driven content in ProcessWire


ryan

Recommended Posts

You can detect whether the current page was loaded from ajax by checking the value of $config->ajax from your template file:

<?php
if($config->ajax) {
    // page was requested from ajax
}

Following that, you will likely want to render the page differently to accommodate whatever you are doing from the javascript side. For instance, you might want do one of these:

  • 1. Deliver alternate or reduced markup when loaded from ajax
  • 2. Deliver a JSON or XML string for parsing from javascript

Below are examples of each of these scenarios.

1. Deliver alternate or reduced markup when loaded from ajax

You might find checking for ajax helpful when you want portions of pages to load in your site without re-rendering the entire page for each request. As a simple example, we'll use the default ProcessWire site and make it repopulate it's #bodycopy area when you click a page in the top navigation. (To use this example, you'll need the default ProcessWire site templates, though you can easily adapt the example to another situation.)

To accomplish this, we'll update our main page template to only include the header and footer markup if the page is NOT being loaded from ajax:

/site/templates/page.php

<?php
if(!$config->ajax) include("./head.inc");
echo $page->body;
if(!$config->ajax) include("./foot.inc");

Next we'll update the top navigation to do ajax loads of the pages when the client has javascript (and leave as-is when they don't). Paste this javascript snippet before the closing </head> tag in the header markup file:

/site/templates/head.inc:

<script type="text/javascript">
    $(document).ready(function() {
        $("#topnav a").click(function() {

            $("#topnav a.on").removeClass('on'); // unhighlight selected nav item...
            $(this).addClass('on'); // ...and highlight new nav item
            $("#bodycopy").html("<p>Loading...</p>"); 
            
            $.get($(this).attr('href'), function(data) { 
                $("#bodycopy").html(data); 
            }); 

            return false; 
        }); 
    }); 
</script>

Now when you click on any page in the top navigation, it pops into the bodycopy area without a page load visible from your browser. And all pages remain accessible from their URL as well. Note that this is just a test scenario, and I probably wouldn't use this approach for the entire bodycopy area on a production site (it would make bookmarking difficult). But this approach can be very useful in the right places.

2. Deliver a JSON or XML string for parsing from javascript

Lets say that you want pages in your site to return a JSON string with the page's id, title, and number of children when it is requested from ajax. When not requested from ajax, they will return their content as normal.

To handle the ajax requests, you'd want to add something like this at the top of your template file before any other output.

<?php
if($config->ajax) {
    // this is an ajax request, return basic page information in a JSON string
    $json = array(
        'id' => $page->id,
        'title' => $page->title, 
        'numChildren' => $page->numChildren
        ); 
    echo json_encode($json);
    return;
}
// not ajax, continue with regular page output

And here is some markup and inline javascript you might use to test the ajax call on some other page (or the same one if you prefer). You would paste this snippet right in your site's markup where you want that info to appear.

<ul id='info'></ul>
<script type='text/javascript'>
    var url = '/'; // this is homepage, so replace '/' with page URL you want to load JSON from
    $(document).ready(function() {
        $.getJSON(url, function(data) {
            $.each(data, function(key, value) {
            $("#info").append("<li>" + key + ": " + value + "</li>");
        }); 
    }); 
}); 
</script>

The above snippet would output something like this:

• id: 1
• title: Home
• numChildren: 5

To take this example further, you could build an ajax-driven sitemap or any number of web services.

Conclusion

Hope this helps you to see how simple it is to use ProcessWire to deliver output for ajax. These are just contrived examples, but hopefully examples that might lead to more ideas. In addition, much of what you see in these examples is also applicable to building web services in ProcessWire.

  • Like 22
Link to comment
Share on other sites

Thanks for this Ryan, very helpful. I actually have used this function to define if page was loaded from ajax:

function isAjax() {
   return (isset($_SERVER['HTTP_X_REQUESTED_WITH'])
              &&
            ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'));
}

But I assume $config->ajax does about the same (checks the headers for XMLHttpRequest)?

Also: it would make great module to return all fields as JSON (also Page references). It would make super simple to create read API:s with PW (not that it is difficult now).

Link to comment
Share on other sites

That $config->ajax is determined exactly like the snippet you pasted, so your isAjax() function and $config->ajax are the same thing.

While there isn't yet a function to convert a page to an array or JSON, I agree that it's something we should add. In the short term, here's one way you can convert a page's data to JSON:

<?php

function pageToJSON(Page $page) {

    $data = array(
        'id' => $page->id,
        'name' => $page->name,
        'status' => $page->status,  
    );

    foreach($page->fields as $field) {
        $name = $field->name; 
        $value = $page->getUnformatted($name); 
        $data[$name] = $field->type->sleepValue($page, $field, $value); 
    }

    return json_encode($data); 
}

(written in the browser, so not yet tested, but I think it should work)

That $field->type->sleepValue function converts whatever the value is to a native PHP type, suitable for storage in a database or JSON (or some other type). For simple text-based types, the value probably won't be changed by sleepValue. But for more complex types like files, images, page references, etc., it converts them to an array representation.

Link to comment
Share on other sites

  • 3 months later...

I'm currently prototyping an new website at work, I'm using jquery ajaxy plugin to provide browser forward/back function and make pages bookmarkable and still function without javascript ( for search engines ). I used $config->ajax in templates to exlude header and footer.

currently available at: http://zueblin.update.ch.update.nine.ch

Link to comment
Share on other sites

Works nicely! Only minor complain is that if you go first to: http://zueblin.update.ch.update.nine.ch/unternehmen/portrait/ and start navigation from there you end up with urls like: http://zueblin.update.ch.update.nine.ch/unternehmen/portrait/#/unternehmen/aktie-zihag/

As a general I don't like hashtag urls at all. Although there really isn't better options for older browsers. In one project I am working on I decided to use ajax to load subcontent. I use HTML5 History API there to keep location bar up to date and allow good back & forward behavior. And browsers that don't support it have it old school - just normal urls.

But making templates to respond to ajax queries is a breeze when using pw :)

Link to comment
Share on other sites

Works nicely! Only minor complain is that if you go first to: http://zueblin.update.ch.update.nine.ch/unternehmen/portrait/ and start navigation from there you end up with urls like: http://zueblin.update.ch.update.nine.ch/unternehmen/portrait/#/unternehmen/aktie-zihag/

I'm aware of this, just not sure yet how to handle, working on it...

As a general I don't like hashtag urls at all. Although there really isn't better options for older browsers. In one project I am working on I decided to use ajax to load subcontent. I use HTML5 History API there to keep location bar up to date and allow good back & forward behavior. And browsers that don't support it have it old school - just normal urls.

I know, but ajaxy doesn't support his new history.js yet, which supports HTML5 history and gracefully degrades on older browsers using hash's. Using history.js alone wouldn't work, beacuse it doesn't support multilevel, forms and json out of the box.

I d'love to hear about other ways doing this in a simple way if anybody knows, cause I've read that HTML5 history ist buggy and inconsistent across browsers...

Link to comment
Share on other sites

I d'love to hear about other ways doing this in a simple way if anybody knows, cause I've read that HTML5 history ist buggy and inconsistent across browsers...

Probably the best and simplest way how you are doing it now. My use case was much simpler, just loading few subsections (taking href attribute and loading that content with ajax to #content div, updating url with history API (without any plugins etc).

Nice looking site it is (although images could be sharper, maybe too heavily optimized?). Great you got your urls working fine, tested and works!

Link to comment
Share on other sites

Nice looking site it is (although images could be sharper, maybe too heavily optimized?). Great you got your urls working fine, tested and works!

Thanks apeisa, well it's still a prototype version with dummy images. Some are too small and just scaled to fit.

Link to comment
Share on other sites

  • 1 month later...

Hello, I just started with PW. It's great - it's natural.

Regarding this subject, I'm unable to get $templates->some_template when in ajax call  ($config->ajax).

The code works fine when not in ajax call.

Is this on purpose?

If so, how am I to save a bunch of field entries using this method http://processwire.com/talk/index.php/topic,346.msg2157.html#msg2157 ?

Thanks! :-\

Link to comment
Share on other sites

  • 1 month later...

Just thought I'd share how I'm doing an all-AJAX site in ProcessWire: For all I know, this may actually be a bad idea, but so far it seems to be working. :)

I'm rendering my nav with anchor tags for hrefs, and storing the actual source URI in an HTML5 arbitrary element attribute. When the user clicks, the browser just takes it as a normal anchor, but the JavaScript pulls the actual source URI out of the link's arbitrary attribute into a jQuery .get() and displays it in the DOM. The advantage is that since this is an AJAX-only site, the user doesn't see the actual path to the content (in the status bar or browser tooltip), which could confuse them (if they were to open it in a new tab, they'd just see unformatted text).

Link to comment
Share on other sites

Thanks for posting Statestreet. It sounds like a good and clever method to me, and I imagine it gives an nice app-like feel. Feel free to post a URL to it if you want. Like with anything ajax, you probably want an alternate/accessible path to pages if you want the content indexed by search engines, but I'm guessing you already know that. :)

Link to comment
Share on other sites

Yeah, I'm not super concerned about deep indexing, as this won't be a huge site (and has an unusual enough name that the coming-soon page is already #3 on Google). It's the kind of site I would have built in Flash a few years ago, so I'm already a step ahead. ;)

That said, I wonder if there's a way to get search engines to index the naked AJAX content pages, and then have those redirect to the main page + appropriate anchor when they click through.

Also, I just wanted to say thanks for making ProcessWire! It's quickly becoming my favorite CMS.

Link to comment
Share on other sites

Also, I just wanted to say thanks for making ProcessWire! It's quickly becoming my favorite CMS.

Thanks Statestreet!

That said, I wonder if there's a way to get search engines to index the naked AJAX content pages, and then have those redirect to the main page + appropriate anchor when they click through.

Here is the most common and accessible way of doing it in ProcessWire (using $config->ajax). Your template that renders the ajax-loaded pages would look something like this:

<?php
if(!$config->ajax) include("./head.inc"); // include full site header if not ajax
echo $page->body; // output all content here that is common to ajax and full version
if(!$config->ajax) include("./foot.inc"); // inclue full site footer if not ajax

Then in your site, output all the links with href attributes pointing to each page's URL. You might output nav like this:

<?php
foreach($pages->get("/")->children() as $child) {
    echo "<a class='ajax' href='{$child->url}'>{$child->title}</a>";
}

From the JS/jQuery side, you would capture the click event on all class="ajax" links and let jQuery pull the content in and populate the container (called #content in this case):

$(document).ready(function() {
    $("a.ajax").click(function() {
        $.get($(this).attr('href'), function(data) {
            $("#content").html(data); 
        }); 
    }); 
}); 

Using this method, clients that have JS/ajax support get the ajax version of the site. Clients that don't have it get a 100% regular and accessible non-ajax site. The only thing that you had to do was have your template check $config->ajax to determine if it was being loaded by ajax, and respond with the appropriate output.

  • Like 3
Link to comment
Share on other sites

  • 1 year later...

Digging up an old topic I know, but I had a need to have a module do some AJAX to request some content from itself - the details of that are irrelevant for now, but you can have a module intercept a page request so that you can have all your PHP code from the AJAX request sitting neatly in the module itself.

For example, if your module's JS file sends an AJAX request on click of a certain element and you want it to return some data, you can do this in your module's JS file:

$('#myelement').load('ModuleName-ajax?do=something');

And then you can intercept that request to yoursite.com/ModuleName-ajax by putting a line like this in your module's init()

if (strpos($_SERVER['REQUEST_URI'],  '/ModuleName-ajax') !== FALSE && isset($this->input->get->do)) {
    echo "Nice!";
    exit;
}

And of course this then echo's "Nice!" into the leement with the ID "myelement" in your HTML.

The only reason I used the module name followed by -ajax was to make sure that that would never be a page on the site. It just avoids confusion if you made a site about modules :)

I wonder if someone will tell me there's a better way now :D

  • Like 1
Link to comment
Share on other sites

Isn't this like uber-hacky way to do stuff?

If I had to implement this, I'd probably do something like /tools/ajax-router, which would then in return go to modules and get stuff by some rules, you know:

<?php /* ajax-router.php */  $function_name = $this->input->get_sanitized_value(); //obvious pseudocode is obvious  $function_name();   function MyModule(){    $myModule = wire('modules')->get('MyModule');    $myModule->serve_this_request($parameters);  }

and be done with it. I might be wrong though :)

Link to comment
Share on other sites

Yep, normally I would also stick something in a folder called /site/tools or something like that.

The problem I'm facing is that I'm trying to do this with an installable module and keep it tidy, but you cant run scripts from the module directory.

I'm open to suggestions but those are the limitations I'm working with.

Link to comment
Share on other sites

Ah, well I wouldn't mind something like that being included in the core distribution to make it possible for any module to call AJAX from itself if that's what you mean? If it was core it would probably also be more secure than some of the stuff I write :D

Link to comment
Share on other sites

  • 1 month later...

You can detect whether the current page was loaded from ajax by checking the value of $config->ajax from your template file:

<?php
if($config->ajax) {
    // this is an ajax request, return basic page information in a JSON string
    $json = array(
        'id' => $page->id,
        'title' => $page->title, 
        'numChildren' => $page->numChildren
        ); 
    echo json_encode($json);
    return;
}
// not ajax, continue with regular page output

Shouldn't $page->numChildren be $page->children->count() ?

You don't want the unpublished and unauthorized pages, right?

Link to comment
Share on other sites

$page->numChildren is a whole lot faster and more efficient than $page->children->count(). In a case like this, I would stick with numChildren. It's okay if it's occasionally wrong with published or otherwise non-accessible page inclusions. Also $page->children->count() is one of those things you really can't use at the large scale, because it has to load all those pages. If you just need to check if a page has accessible children (regardless of amount), you can do: 

$hasChildren = $page->child->id > 0; 

To count all visible children:

$numChildren = $pages->count("parent=$page"); 

I'll add a $page->numChildrenVisible property in the next commit. 

  • Like 4
Link to comment
Share on other sites

  • 1 month later...

Hi forum!

i have this problem, my first site on proccess wire uses ajax and it works fine in localhost wamp, but when i move it online i have issues with the urls not working, after read this topic and research on internet i try all the solutions i find but no luck, i use the help from topic here, but nothing, the problem with links

i think the problem come from the # symbol in urls any idea how to make it work?

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...