Project

Profile

Help

Support #4732

Upgrading AtomGraph application from Saxon-CE to Saxon-JS 2

Added by Martynas Jusevicius 14 days ago. Updated 1 day ago.

Status:
In Progress
Priority:
Normal
Category:
-
Sprint/Milestone:
-
Start date:
2020-09-11
Due date:
% Done:

0%

Estimated time:
Applies to JS Branch:
2.0
Fix Committed on JS Branch:
Fixed in JS Release:
SEF Generated with:
Company:
-
Contact person:
-
Additional contact persons:
-

Description

As explained on the mailing list, I am trying to replicate the behavior of Saxon-CE's updateHTMLDocument() with Saxon-JS 2. This code is invoked from a JS onsubmit handler in order to replace the content of the submitted form:

SaxonJS.transform({
    "stylesheetLocation":
"https://localhost:4443/static/com/atomgraph/linkeddatahub/xsl/client.xsl.sef.json",
    "initialMode":
"Q{https://w3id.org/atomgraph/linkeddatahub/domain#}ConstructViolation",
    "logLevel": 10,
    "sourceNode": html, // HTML document node from a jqXHR response
    "templateParams": {
        "submitted-form": form // the onsubmit target
    },
    "destination": "raw",
    "nonInteractive": true
}, "async");
<xsl:template match="html" mode="apl:ConstructViolation">
    <xsl:param name="submitted-form" as="element()"/>
    <xsl:variable name="violation-form" as="element()">
    ...
    </xsl:variable>

    <xsl:result-document href="#{$submitted-form/@id}" method="ixsl:replace-content">
        <xsl:copy-of select="$violation-form/*"/>
    </xsl:result-document>
</xsl:template>

However with each invocation it looks like the ixsl: event listeners are being registered again. I don't need this since this code only handles onsubmit (since Saxon-JS cannot handle it natively), and there's another "main" SaxonJS.transform() that already has registered the listeners. I thought I could switch this off using "nonInteractive": true, but it does not seem to have the desired effect.

History

#1 Updated by Michael Kay 14 days ago

  • Category set to Internals
  • Assignee set to Debbie Lockett
  • Priority changed from Low to Normal

#2 Updated by Debbie Lockett 14 days ago

Thanks for raising here, rather than just on the saxon-help list. There is obviously quite a lot going on in your application, so there is a lot to understand about what you are doing, and what you are trying to do. This makes it hard to pin down the separate issues.

Certainly you don't want events registered multiple times. Indeed "nonInteractive": true is supposed to switch off registering the event listeners, so we'll need to investigate why that's not working. (Perhaps it is because you have multiple SaxonJS.transform() calls?)

I'm not sure that the second call to SaxonJS.transform() is really the right approach.

You say that onsubmit is not handled by Saxon-JS, you may be right, but I'm not sure. Do you mean the actual form submission? In which case I assume your form submits with an HTTP POST? So then the problem is with capturing the response? Hmm, yes I'm not sure how you would do that...

You can use ixl:schedule-action/@http for HTTP, but there you are directly specifying the request and the response handling; whereas I guess with an HTML form the request is internal, so managing the response becomes awkward...?

I may need to review more of your application (as well as refer back to previous work on Saxon-Forms, our partial Saxon-JS implementation of XForms, to remind myself of similar work) to help. The work of upgrading your Saxon-CE application to Saxon-JS 2 is clearly quite an undertaking, and needs time and effort. (Upgrading our Saxon documentation applications has similarly involved work and effort; but spread over a much longer period of development!) It sounds like with Saxon-CE you already had to do quite a bit of juggling IXSL and pure JavaScript. Hopefully there is now more you can do directly in IXSL with Saxon-JS 2, but working that out might not be immediate.

#3 Updated by Martynas Jusevicius 14 days ago

Hi Debbie. The project in question is this (the develop branch): https://github.com/AtomGraph/LinkedDataHub/ It's deployed here (but you would need to sign up): https://linkeddatahub.com:4443/

Let me try to explain the use case bit better.

The main SaxonJS.transform() is in the HTML document <head>. It registers the events that all works fine.

Now, when a form is POSTed, it is validated on the server, and another version is rendered with validation errors. The idea is to take the new form with errors and replace the contents of the submitted form with it, so that from the user perspective the validation would look seamless.

One problem was that onsubmit was not handled by Saxon-CE. My new test shows that Saxon-JS does indeed catch it: https://namedgraph.github.io/saxon-js2-test/ Source: https://github.com/namedgraph/saxon-js2-test/blob/gh-pages/client.xsl

I've added <xsl:sequence select="ixsl:call(ixsl:event(), 'preventDefault', [])"/> to prevent the actual request. Is this the right approach?

Given that Saxon-JS can handle onsubmit, I'll try a native solution and get rid of the JS altogether.

#4 Updated by Martynas Jusevicius 14 days ago

Ah, but how do I get POST request body, i.e. the application/x-www-form-urlencoded or multipart/form-data encoded data of the submitted form? The relevant JS code looks like this:

if (this.enctype === "multipart/form-data")
    settings = 
    {
        "method": this.method,
        "data": new FormData(this),
        "processData": false,
        "contentType": false,
        "headers": { "Accept": "text/html" }
    } ;
else settings = 
    {
        "method": this.method,
        "data": $(this).serialize(), // jQuery method
        "contentType": "application/x-www-form-urlencoded", // RDF/POST
        "headers": { "Accept": "text/html" }
    } ;

$.ajax(this.action, settings)....

Maybe I could use FormData somehow... Need to think about this.

#6 Updated by Martynas Jusevicius 11 days ago

I've made good progress implementing the same logic in XSLT, but got stuck on multipart requests.

I can successfully construct a FormData instance from the form and use it as 'body' in http-request.

However, I cannot specify 'media-type': 'multipart/form-data' because the browser adds boundary to Content-Type.

The solution seems to be to not set Content-Type explicitly and let the browser do it. That worked in my JS code, but Saxon-JS complains if media-type is not set:

code: "SXJS0006"
​
message: "No content type specified in HTTP request to: https://localhost:4443/files/?forClass=https%3A%2F%2Flocalho…mode=https%3A%2F%2Fw3id.org%2Fatomgraph%2Fclient%23ModalMode"
​
name: "XError"

I realize this is now a completely different issue than the original one :) Should I open another one or should we change the title?

#7 Updated by Martynas Jusevicius 11 days ago

I tried specifying an empty sequence (<xsl:map-entry key="'media-type'" select="()"/>) but got the same error...

#8 Updated by Debbie Lockett 11 days ago

  • Tracker changed from Bug to Support
  • Subject changed from Events registered multiple times to Upgrading AtomGraph application from Saxon-CE to Saxon-JS 2
  • Category deleted (Internals)
  • Status changed from New to In Progress

Glad you've been making progress!

Ah sorry, this is a current limitation of the Saxon-JS HTTP interface: multipart HTTP requests are not currently implemented. I guess we haven't been aware of anyone needing them so far; but we did know that this should be provided one day...

Can you come up with another work around (perhaps the FormData be serialized, send as text, and then parsed again server side? Or is that too messy?); or is this a blocker?

Regarding the bug issue title etc. - I've made some changes. Let's treat this one as a "Support" issue where we can discuss anything relating to the work to upgrade your application from Saxon-CE to Saxon-JS 2. We can spin off specific bugs for issues you find, as and when they crop up (and try to keep them focussed!)

#9 Updated by Martynas Jusevicius 11 days ago

I noticed the multipart- keys but I don't need to use them - FormData works fine.

I'm not going to implement any specific server-side processing for this. If anything, I'll need to revert back to JS for multipart requests.

How about making media-type optional in http-request? Unsetting Content-Type and letting XMLHttpRequest handle it should solve this issue, please see here: https://stackoverflow.com/questions/39280438/fetch-missing-boundary-in-multipart-form-data-post

#10 Updated by Debbie Lockett 11 days ago

Oh, sorry perhaps I've jumped to the wrong thing there. I was assuming that with 'media-type': 'multipart/form-data' this meant that you'd be using multipart bodies. I haven't yet looked closely at what a FormData object really looks like; and hadn't clicked when you said you'd use FormData for the request body.

But yes, it looks like there is some bug here which means sending FormData doesn't work... I'll investigate.

#11 Updated by Debbie Lockett 11 days ago

By the way, I've raised the following related issues [so far ;-)] :

  • Bug #4728: Coverage for event handling
  • Bug #4734: SaxonJS.transform nonInteractive option does not work
  • Feature #4735: Add multipart support in HTTP client
  • Feature #4736: Add syntax for an external JavaScript object type

#12 Updated by Martynas Jusevicius 10 days ago

Here's the relevant code: https://github.com/AtomGraph/LinkedDataHub/blob/develop/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl#L1121

I'm using either URLSearchParams or FormData as the request body, depending on the form's enctype.

For multipart/form-data, the JS solution is to not set the Content-Type header so that the browser can do it with the correct boundary etc. That is what the StackOverflow answer explains.

However I cannot do that with <ixsl:schedule-action http-request=""> because media-type is mandatory.

#13 Updated by Martynas Jusevicius 4 days ago

Any news? Multipart forms is one of the few remaining blockers for a new release of LinkedDataHub which uses Saxon-JS instead of Saxon-CE.

#14 Updated by Debbie Lockett 4 days ago

Sorry, other issues have occupied my time recently, so I have not looked much further.

Making media-type optional rather than mandatory would involve changes to the Saxon-JS HTTP client, so this would only be available in a new release. And we don't know how soon such a release will be. Anyway, I'd need to look into it more to see whether that is what we should do. The design of the HTTP client was based on the EXPath HTTP module, where the media-type of the request body is mandatory.

I actually assumed that you would revert back to using JS for the multipart forms for now, to work around this?

#15 Updated by Martynas Jusevicius 3 days ago

Well on a second thought I don't know if I have a fallback JS solution for this. Say I invoke fetch() and pass FormData in the case of a multipart request. How do I handle the callback? With Saxon-CE I did updateHTMLDocument() but if I use SaxonJS.transform() a second time, I'll get events registered twice.

Saxon-CE also allowed defining callbacks as XSLT templates. For example, I could call <xsl:sequence select="ac:fetch($select-uri, 'application/rdf+xml', 'onContainerQueryLoad')"/> where <xsl:template match="ixsl:window()" mode="ixsl:onContainerQueryLoad"> would be the callback. However my test shows that this pattern is not available in Saxon-JS. I tried the following:

    <xsl:template match="button[@id = 'custom-handler']" mode="ixsl:onclick">
        <xsl:message>CUSTOM HANDLER BUTTON</xsl:message>

        <xsl:sequence select="ixsl:call(js:fetch('test.xml'), 'then', [ ixsl:get(ixsl:window(), 'oncustomHandler') ])"/>
    </xsl:template>

    <xsl:template match="." mode="ixsl:oncustomHandler">
        <xsl:message>CUSTOM HANDLER CALLBACK</xsl:message>
    </xsl:template>

but got Warning ixsl:get: object property 'oncustomHandler' not found. Unless the test is wrong?

So I'm running out of ideas :/

As for media-type, unsetting Content-Type is a peculiar trick to get the browsers to set the correct multipart headers themselves. In theory it shouldn't work but it does... Because there doesn't seem to be another way to calculate the boundary in code. How about passing () as media-type to unset the header?

#16 Updated by Debbie Lockett 1 day ago

Yes, I think there are some issues with your test. I'd propose something like the following:

    <xsl:template match="button[@id = 'custom-handler']" mode="ixsl:onclick">
        <xsl:message>CUSTOM HANDLER BUTTON</xsl:message>

        <xsl:sequence select="js:customFetch('test.xml')"/>
    </xsl:template>

    <xsl:template match="." mode="ixsl:oncustomEvent">
        <xsl:message>CUSTOM HANDLER CALLBACK</xsl:message>
    </xsl:template>

Where customFetch is a custom global JavaScript function, which invokes fetch() as required, and its callback triggers the customEvent event. (I would write this callback using then directly in JavaScript, there is no benefit to doing it using IXSL, and in fact I think that just confuses things.)

The match="." mode="ixsl:oncustomEvent" template will handle the customEvent, and you can use ixsl:event() to access customEvent information (e.g. the response details).

(FYI A couple of the issues with ixsl:call(js:fetch('test.xml'), 'then', [ ixsl:get(ixsl:window(), 'oncustomHandler') ]):

  1. it should be ixsl:call(fetch('test.xml'), ...

  2. This is trying to call a global function called oncustomHandler, but you want to be triggering a customHandler event to be handled by the mode="ixsl:oncustomHandler" template. So instead you should call a custom JS global function which triggers the event.)

#17 Updated by Martynas Jusevicius 1 day ago

Thanks! I'll give it a try.

"Trigger an event" -- do you mean using dispatchEvent() here?

#18 Updated by Debbie Lockett 1 day ago

Yes, dispatchEvent(). So something like:

const event = document.createEvent('Event');
event.initEvent('customEvent', true, true);
// no need to add event listeners here, that is done by IXSL

document.dispatchEvent(event);

(I'm referring to https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events.)

Please register to edit this issue

Also available in: Atom PDF Tracking page