MindTouch ships with jQuery, which is a versatile JavaScrpt library for doing a lot of things. Primarily, it focuses on DOM element manipulation, making it easier to create compelling, dynamic pages.  But jQuery also includes AJAX functionality for interacting with web-services, such as the MindTouch API.

Most of the jQuery AJAX functions aren't that interesting, because they are only intended for the most basic cases.  For this write-up, we're more interested in some of the advanced use cases that require both a good understanding of AJAX and the MindTouch API.  For sake of concreteness, the samples show how to interact with page properties for creating, retrieving, updating, and deleting values.

Things to know before you begin making your AJAX request:

  1. First, you'll need to know what HTTP verb to use when accessing the API.  Usual choices are GET, POST, PUT, and DELETE.  There are others, but you'll likely not run into them.
  2. Second, you need to know the URI to send the request to.  It's best not to hard-code the URI in your solution.  Instead, use a DekiScript snippet to retrieve the appropriate URI dynamically (e.g. {{ page.api }} or {{ site.api }})
  3. Third, you need to know what data are you going to send (if any).  Most common are text and XML documents.
  4. Fourth, is there any information you may need to send as HTTP headers (e.g. ETag, Slug, etc.).  Don't worry about HTTP cookies.  The browser will take care of those for you.
  5. Lastly, you need to know the various response codes that the API may return.  Depending on the status code, your application may need to react differently.  A status code of 200 means success.  Other status codes may indicate partial success or failure.

Lastly, it needs to be pointed out that the samples use JEM, which is a JavaScript pre-processor that's built into MindTouch.  JEM makes it easier to mix DekiScript and JavaScript on wiki pages, and react to user input, such as button clicks.  JEM is only available in MindTouch Core 9.02 and MindTouch 2009, or later.  However, the shown jQuery functionality has been available since MindTouch 8.08.

Sample 1: Create a new property

Use the POST:pages/{pageid}/properties API to create a new property.  The API expects the name of the new property to be supplied in a custom header named Slug.

You can use any name you like, as long as it's non-empty.  It's recommended that you use a prefix for your property as well.  In our examples, we call our property "sample-property" and we use the custom namespace prefix "urn:custom.mindtouch.com".  The full property name then becomes: "urn:custom.mindtouch.com#sample-property" (w/o the quotes).  Using the custom namespace prefix allows our property to be shown on the "Page Properties" page.

Unless you know what you're doing, it's recommended you use the "urn:custom.mindtouch.com" prefix as well!

Example

The following button will try to create a new property on the current page.  The operation will fail if the property already exists or if you do not have permission to create a property.

Code

Here's the code for showing a button to create a property.

<input type="button" value="Click me to create a property." ctor="when($this.click) {

    // send AJAX request using jQuery
    $.ajax({
    
        // set the uri for the properties API of the current page
        url: {{ page.api }} + '/properties',
        
        // set the request HTTP verb
        type: 'POST', 
        
        // set the value of the new property
        data: 'This is the new value!', 
        contentType: 'text/plain',
        processData: false,
        
        // add the 'Slug' header which sets the property name
        beforeSend: function(xhr) { 
            xhr.setRequestHeader('Slug', 'urn:custom.mindtouch.com#sample-property'); 
            return true; 
        },
        
        // check outcome of the request
        complete: function(xhr) {

            // check the response status code
            if(xhr.status == 200) {
                alert('Property was successfully created.');
            } else {
                alert('Unable to create the property. It may already exist (try deleting it) or you don\\'t have permission to create it.');
            }
        }
    });
}" />

Sample 2: Read a property

To read a property, we need to query the properties collection using the GET:pages/{pageid}/properties API.  We limit the set of returned properties by using the names query parameter.

Also, to make our lives easier, we indicate to the API that we want the result to be returned as a JSON object instead of an XML document.  Once the property is read, we display it an alert message.

Example

The following button queries the properties of the current page and tries to find the property created by the previous sample.  If the property is found, it's current value is displayed in an alert box.

Code

Here's the code for showing a button to read a property.

<input type="button" value="Click me to read a property" ctor="when($this.click) {

    // property name to read
    var name = 'urn:custom.mindtouch.com#sample-property';

    // send AJAX request using jQuery
    $.ajax({
    
        // set the uri for the properties API of the current page
        // add parameter to only list properties that match the requested name
        // add parameters to convert response to JSON and
        url: {{ page.api }} + '/properties?dream.out.format=json&names=' + Deki.url.encode(name), 
        
        // set the request HTTP verb
        type: 'GET',
        cache: false,
        
        // check outcome of the request
        complete: function(xhr) {
        
            // check response status code
            if(xhr.status == 200) {
            
                // evaluate response JSON data
                var data = eval('(' + xhr.responseText + ')');

                // read property value
                var value = data.property && data.property.contents['#text'];
                
                // check we the value was found
                if(value) {
                    alert('Property was successfully read.\\nIts value is \\'' + value + '\\'');
                } else {
                    alert('Unable to find the property. It may not exist (try creating it).');
                }
            } else {
                alert('Unable to query the property. You don\\'t have permission to read it.');
            }
        }
    });
}" />

Sample 3: Update a property

To update a property, we must first read it to obtain its location and ETag.  The update operation is then performed using the PUT:pages/{pageid}/properties/{key} API.  The ETag value is provided as a HTTP header, similar to the Slug header when we created the property.

The ETag ensure that we don't update a value that may have changed since we read it.  In other words, it provides a mechanism for detecting a conflict in updating a property.  How the conflict should be handled depends on the situation.  For example, if the property contains a list of values, and the intent was to append another value, then simply re-read the property, re-append the value, and try to update it again.

Example

The following button will try to read the property and its ETag and then update its value.  The operation will fail if the property does not exist, you do not have the permission to update it, or it has been modified by someone else between the time it was read and the update attempt was made.

Code

Here's the code for showing a button to update a property.

You may notice that the update operation is done using a POST instead of a PUT.  The reason is that many browser only support GET and POST for AJAX requests.  However, the MindTouch API allows you to specify the HTTP verb in the URI when doing a POST request to circumvent this limitation.

<input type="button" value="Click me to update a property" ctor="when($this.click) {

    // property name to update
    var name = 'urn:custom.mindtouch.com#sample-property';

    // send AJAX request to find the property
    $.ajax({
    
        // set the uri for the properties API of the current page
        // add parameter to only list properties that match the requested name
        // add parameters to convert response to JSON and
        url: {{ page.api }} + '/properties?dream.out.format=json&names=' + Deki.url.encode(name), 
        
        // set the request HTTP verb
        type: 'GET',
        cache: false,
        
        // check outcome of the request
        complete: function(xhr) {
        
            // check response status code
            if(xhr.status == 200) {
            
                // evaluate respone JSON data
                var data = eval('(' + xhr.responseText + ')');

                // read property href and ETag
                var href = data.property && data.property.contents['@href'];
                var etag = data.property && data.property['@etag'];
                
                // check we the value was found
                if(href && etag) {

                    // send AJAX request to update the property
                    $.ajax({ 
        
                        // set the request HTTP verb
                        url: href + '?dream.in.verb=PUT',
                        type: 'POST', 
        
                        // set the value of the updated property        
                        data: 'This is the updated value!', 
                        contentType: 'text/plain',
                        processData: false,

                        // add the 'ETag' header which checks if the property was modified since we read it
                        beforeSend: function(xhr) {

                            // NOTE (see Update 1 below): remove content-encoding added by Apache mod_deflate
                            etag = etag.replace('-gzip', '').replace('-bzip2', '').replace('-zip', '');

                            xhr.setRequestHeader('ETag', etag); 
                            return true; 
                        },
        
                        // check response status code
                        complete: function(xhr) {

                            // check the response status code
                            if(xhr.status == 200) {
                                alert('Property was successfully updated.');
                            } else {
                                alert('Unable to update the property. It may no longer exist (try recreating it), it may have been changed, or you don\\'t have permission to update it.');
                            }
                        }
                    });
                } else {
                    alert('Unable to find the property. It may not exist (try creating it).');
                }
            } else {
                alert('Unable to query the property. You don\\'t have permission to read it.');
            }
        }
    });
}" />

Sample 4: Delete a property

Finally, we can delete a property using the DELETE:pages/{pageid}/properties/{key} API.  The delete request does not require an ETag since it really doesn't matter if the property has changed since it was read.  However, we still need to find its location.

Example

The following button will delete the property created by the first sample.  The operation will fail if the property does not exist or if you don't have permission to delete it.

Code

Here's the code for showing a button to delete a property.

<input type="button" value="Click me to delete a property" ctor="when($this.click) {

    // property name to update
    var name = 'urn:custom.mindtouch.com#sample-property';

    // send AJAX request to find the property
    $.ajax({
    
        // set the uri for the properties API of the current page
        // add parameter to only list properties that match the requested name
        // add parameters to convert response to JSON and
        url: {{ page.api }} + '/properties?dream.out.format=json&names=' + Deki.url.encode(name), 
        
        // set the request HTTP verb
        type: 'GET',
        cache: false,
        
        // check outcome of the request
        complete: function(xhr) {
        
            // check response status code
            if(xhr.status == 200) {
            
                // evaluate respone JSON data
                var data = eval('(' + xhr.responseText + ')');

                // read property href
                var href = data.property && data.property.contents['@href'];
                
                // check we the value was found
                if(href) {

                    // send AJAX request to delete the property
                    $.ajax({ 
        
                        // set the request HTTP verb
                        url: href + '?dream.in.verb=DELETE',
                        type: 'POST', 

                        // check response status code
                        complete: function(xhr) {

                            // check the response status code
                            if(xhr.status == 200) {
                                alert('Property was successfully deleted.');
                            } else {
                                alert('Unable to delete the property. It may not exist (try creating it) or you don\\'t have permission to delete it.');
                            }
                        }
                    });
                } else {
                    alert('Unable to find the property. It may not exist (try creating it).');
                }
            } else {
                alert('Unable to query the property. You don\\'t have permission to read it.');
            }
        }
    });
}" />

Sample 5: Set Page security

Use the POST:pages/{pageid}/security API to set new security permissions.  The API expects the permission in XML format, so we will have to play with JQuery to create this XML.

This XML allows for a expiration date for each permissions set.

Example

The following button will try to add a new security restriction on the current page.  The operation will fail if the restriction already exists or if you do not have permission to add a security resitriction.

Code

Here's the code for showing a button to add a security restriction.

<input type="button" value="Set Page Security" ctor="
	$this.click(function(){
		var userid = {{ user.id }};
		var dateexpires = {{ date.addMinutes(date.now,5) }};
		var page = {{ page.path }};
                var restriction= 'Semi-public';

		// generate the details
		var restrictions = Deki.$('<security></security>');
		$('<permissions.page></permission.page>')
			.append(Deki.$('<restriction>' + restriction + '</restriction>'))
			.appendTo(restrictions);
		var grantsAdded = Deki.$('<grants.added></grants.added>').appendTo(restrictions);
		$('<grant></grant>')
			.append(Deki.$('<permissions></permissions>')
			.append(Deki.$('<role>Contributor</role>')))
			.append(Deki.$('<user></user>').attr('id',userid))
			.append(Deki.$('<date.expires></date.expires>').text(dateexpires))
			.appendTo(grantsAdded);
		restrictions = '<security>' + restrictions.html() + '</security>';
		
		var pageapi = '/@api/deki/pages/=' + Deki.url.encode(Deki.url.encode('DekiScript/FAQ/How_do_I..._Use_jQuery_to_access_the_MindTouch_API_via_AJAX%3f/Page_Security_example'));
		
		// send AJAX request using jQuery
		$.ajax({
			// set the uri for the properties API of the current page
			url: pageapi + '/security',
			
			// set the request HTTP verb
			type: 'POST', 
			contentType: 'text/xml',
			processData: true,
			
			// set the value of the new property
			data: restrictions, 
			
			// check outcome of the request
			complete: function(xhr) {
				// check the response status code
				if(xhr.status == 200) {
					alert('Page property was set successfully.');
				} else {
					alert('Unable to create the security. Perhaps you do not have permission.');
				}
			}
		});
	});

" />

What's next?

Hopefully these samples have given you a solid starting point for using the MindTouch API with AJAX requests.  The samples show all the best practices you should follow when making your own AJAX requests, such as relying on asynchronous callback functions instead of blocking calls.  The latter make for a very poor user experience quickly (though, it may not be apparent during the development cycle, so just trust me on that!).  All response codes are taken into consideration, although one could validly argue that just checking for status code 200 is too strict.  And finally, jQuery's AJAX below-the-covers smarts, such as automatic content transformations and caching, are disabled when needed.

Next, you should read on the full capabilities the ajax() function in jQuery, as well as the MindTouch API.  If you run into any trouble or have something cool to share, make sure to post it on the DekiScript & Deki Extensions forum. 

Update 1: Problems with ETag

User:rberinger has reported an issue on a thread on the forums.  It turns out that Apache servers with mod_deflate installed may modify the value of the ETag by appending the Content-Encoding value to the ETag.  For example, if the ETag value was originally "abc" and the Content-Encoding is "gzip", you will end up with an ETag having the value "abc-gzip".  Needless to say, this then makes the update operation fail since the ETag doesn't match anymore.

The workaround is to strip "-gzip", "-bzip2", and "-zip" from the end of an ETag when they are present.

Viewing 5 of 5 comments: view all
After some difficulties, here's how I did to make it work:
- copy the code from here and paste it into the Source editor
- return to the Normal editore and go back to Source. The editor should have formatted a little bit (added <br/> for example.
- Cut it from the Source and paste on the Normal Editor between {{ }}
Be extremly careful of '&' transformed into '&amp;'. Remove one 'amp;' and it should be fine.

Well, that's how I did, if there is a better solution I'm all ears.

Great code btw, next I will try to make a form to choose the name and the value of the property. edited 19:38, 3 Jun 2009
Posted 19:37, 3 Jun 2009
It seems also that it works better on IE. I have a problem on Firefox for the delete: at line 44, xhr.status == 411.
Posted 21:04, 3 Jun 2009
@Polo Thanks for the feedback. Status code 411 means missing Content-Length. I'll have to run it under FF, though it works fine under Safari. Also, there is a much easier way to copy it. Click the <> icon in the top right corner of the source code. Copy all it. Open a new page. Use the DekiScript style from the styles menu, which creates a <pre> section. Paste the code into the <pre> section in WYSIWYG mode. Save the page. Done!
Posted 00:12, 4 Jun 2009
This is a great article. I wish that it went over more advanced things too, such as how to set a page's security and how to post content to a page (append or overwrite)...
Posted 20:31, 22 Jun 2009
I know i am beginning to be a pain... is there a solution to the FF problem already? or can someone send me to some article on how to set the content length? sorry i am totally out of knowledge of ajax.
Posted 09:50, 17 Jul 2009
Viewing 5 of 5 comments: view all
You must login to post a comment.