[Seaside] Updating browser history from an ajax response

Paul DeBruicker pdebruic at gmail.com
Sat Apr 13 23:04:37 UTC 2019




I fully believe there is a better/less hackish way to do this if its going
to be a seaside feature.  But what I have does to the partial page
update/back button/shareable links stuff well enough for me for now.

You're right I do generally use it for navigation toreplace the content of a
div with the id 'body' but  I also use it to 

    - update the page title
    - load on page help for the current view 
    - move the person along in the guided tour if they're taking one
    - update other indicators/notifications if they've changed.   e.g. "You
have a new message from ..."

I think that the DOM editing code could easily be adapted to also replace an
arbitrary collection of specific DOM components like you'd prefer without
too much trouble.  If you had an id for every DOM element you wanted to
replace in the real DOM and a matching div with the content in the Seaside
ajax response then you could itereate of the received divs putting their
contents into the target ids in the real DOM.  The duplicated ids would only
exist in the response handling code.  And you could update multiple spots on
the page at once for a given round trip to the server. 


The setBodyJs code just searches the received div's putting them in the
correct spots and then runs the scripts that were included in the response.  
Anyway that code is

MyComponent>>#setBodyJs
	^ (JSStream on: '$("#body").setBody(resp, status, xhr);') asFunction:
#('resp' 'status' 'xhr')


 $.fn.setBody = function (response, txtStatus, jqXhr) {
         var content, body = $('#body'), newElement, target;
         newElement=document.createElement('div');
         newElement.innerHTML = response;
         
         content = newElement.querySelector('#b');
         if(content){
                 body.empty().append(content.innerHTML);
         } else {
             window.location.href = window.location.href.split("?")[0];
         };

         content = newElement.querySelector('#h');
         if(content){
             $('#helpBarContent').empty().append(content.innerHTML);
         };

         content = newElement.querySelector('#t');
         if(content){
             $('#tour').empty().append(content.innerHTML);
         };

         content = newElement.querySelector('#tp');
         if(content){
             $('#tourProgress').empty().append(content.innerHTML);
         };

         $(response).filter('script').each(function () {
             $.globalEval(this.text || this.textContent || this.innerHTML ||
'');
         });
         document.title = newElement.querySelector('#ti').innerHTML;
         $viewport.scrollToTop();
 };

So you can see that its just finding elements by id ('#b' '#h' '#t' '#tp'
'#ti') in the Seaside response and replacing others in the DOM and could be
generalized into a loop if you wanted to spend the time to do it (if you
were making it part of an open source package others could use for example
;) ) .  


For keeping the URL in sync I do not use updateUrl: because it only (I
think?) edits the response URL & instead I just edit the url for the anchor
before the pjaxCallback: e.g.

html anchor
     backEndUrl:'mypage';
     pjaxCallback:[self showMyPage] on: html;
     with: 'click me to see my page'.


and WAAnchorTag>>#backEndUrl: is 

backEndUrl: anExtraPath
	self
		useBaseUrl;
		extraPath: MyApp appName;
		extraPath: anExtraPath

By editing the url for the anchor it can be copy and pasted among users both
from the anchor on the page and the url bar in the browser once they've
arrived on the page of interest.  It is only dissected and used in
#initialRequest: when people share links  and when the back button is
clicked as described below. I use cookies for sessions so there isn't a  _s
parameter in the urls that are created.  


To handle the back button I'm overriding the browser's native back button
handling.  There is some js where I add a header when the back button is
clicked, and then look for that in my session class and handle it.

The JS that adds the header is is

window.onpopstate = function (event) {
   // console.log(event.state);
    if ($('body.back').length && event.state) {
      $.ajax({url: event.state, 
                success: function(data, textStatus, jqXhr)
{$('body').setBodyAfterBackButton(data, textStatus, jqXhr);}, 
                beforeSend:
function(xhr){xhr.setRequestHeader("X-Requested-With", "pjaxBB");}});     
    }
}; 

and the setBodyAfterBackButton replaces the existing page with the Seaside
ajax response.

$.fn.setBodyAfterBackButton = function (data, status, jqXhr) {
         var newDoc = document.open("text/html", "replace");
         newDoc.write(data);
         newDoc.close();
};


Then in the session I override #handleFiltered: like this


MySession>>#handleFiltered: aRequestContext
	(self isBackButtonPjaxRequest: aRequestContext)
		ifTrue: [ self handleBackButtonRequest: aRequestContext ]
		ifFalse: [ super handleFiltered: aRequestContext ]


MySession>>#isBackButtonPjaxRequest: aRequestContext
	^ (aRequestContext request headers at: 'x-requested-with' ifAbsent: [ nil
]) = 'pjaxBB'


MySession>>#handleBackButtonRequest: aRequestContext
		| bdy path |
		path := aRequestContext request uri path allButFirst.
		bdy := self bodyFromBackButtonPath: path.
		self  body: bdy.
		aRequestContext request uri addField: '_n'.
		super handleFiltered: aRequestContext


MySession>>#bodyFromBackButtonPath: path
   "App specific code to find which page of the app should be shown and what
content should be in it just like in #initialRequest: "




Thats it I think.  It works. Could be clearer/cleaner/more general.   I'd
prefer it to be more automatic (evidenlty like Iliad I guess) but I'm not
creating so many new things that setting the urls & adding the lookup code
for the #bodyFromBackButtonPath:/#initialRequest: is a burden.  


And like you remember I really only use this for navigation among areas of
the app.  It doesn't remember if you had a modal open or a few accordions
expanded and others collapsed or were editing a thing on one page and
another thing on another page.  And I don't know how to go back to prior
server states with this. It will take you back ten screens in order but that
screen will have the most recently edited data.  Not be in the same state it
was when you first looked at it.   Unlike the counter demo I'm not sure it
should in a multi user app where others might've seen it or been affected by
the changes. 

Hope this helps

Paul





Esteban A. Maringolo wrote
> Hi Paul,
> 
> I've seen that code before ;-) and maybe setting the URL **before** is
> the way to go.
> 
> In your case you always replace the "body" component of your page, I
> might need to adapt it in a way that could replace any DOM element.
> 
> Regarding this:
> "Not sure its what you want to do but it definitely sets the window
> location
> (url) to something that when you hit refresh gives you the same page you
> see
> before you hit refresh (with updated server content, if anything there has
> changed in the interim)."
> 
> I don't remember how you consumed that in #initialRequest and whether
> you specified #updateUrl: (and kept it in sync on each
> "pjaxCallback:..." call).
> 
> Regards,
> 
> Esteban A. Maringolo
> 
> On Fri, Apr 12, 2019 at 1:47 PM Paul DeBruicker <

> pdebruic@

> > wrote:
>>
>> Oh and I forgot that the onClick: is housed in another function that
>> allows
>> the ajax pushState anchors to work even if JS is disabled or there is no
>> session and looks like this:
>>
>>
>> WAAnchorTag>>#pjaxCallback: aBlockClosure on: html withSuccessScript:
>> aScript
>>   self
>>     onClick:
>>         (self
>>             processCallback: aBlockClosure
>>             onSuccess: self setBodyJs , aScript
>>             return: false
>>             thenUpdateBodyOn: html);
>>     callback: aBlockClosure
>>
>>
>> &
>>
>> processCallback: aCallback onSuccess: aJSFunction return: aBoolean
>> thenUpdateBodyOn: html
>>   self hasSession
>>     ifTrue: [
>>       | scr |
>>       scr := html jQuery ajax
>>         html: [ :h |
>>               aCallback value.
>>               self renderUpdatedBodyContentFor: self session pjaxBody on:
>> h
>> ];
>>         onSuccess: aJSFunction.
>>       aBoolean notNil
>>         ifTrue: [ scr return: aBoolean ].
>>       ^ html jQuery this updateUrl
>>         , ((html jQuery class: 'active') removeClass: 'active') , scr ]
>>     ifFalse: [ ^ nil ]
>>
>>
>> So its
>>
>> html anchor
>>     pjaxCallback:[ self doSomethingYourClientsLove ] on: html
>> withSuccessScript: (JSStream on:'alert("I don't believe that worked")');
>>     with: 'Click me'
>>
>>
>>
>>
>> Paul DeBruicker wrote
>> > Oh man if only our past selves knew we really wanted Iliad apps what a
>> > world
>> > this would be....
>> >
>> >
>> >
>> > In my apps I use
>> >
>> >      $.fn.updateUrl = function(anId){
>> >
>> >          if (typeof(window.history.pushState) == 'function') {
>> >              var relativeUrl = $(this).attr('href');
>> >              if(typeof(relativeUrl) =='undefined'){
>> >                  relativeUrl =
>> > document.location.pathname+document.location.search;
>> >              };
>> >              if(relativeUrl!=='javascript:void(0)' && relativeUrl !==
>> > window.history.state){
>> >                  window.history.pushState(relativeUrl, document.title,
>> > relativeUrl);
>> >              }
>> >          }
>> >      };
>> >
>> > and call it as part of the onClick: script on anchors and buttons
>> BEFORE
>> > loading content via ajax.
>> >
>> > So the onClick: is
>> >
>> >
>> > onClick: html jQuery this updateUrl, (self doContentLoadingMagicOn:
>> html)
>> >
>> >
>> > Also it doesn't need to be a jQuery function but is because it was
>> > different
>> > when I initially made it.
>> >
>> >
>> >
>> > Not sure its what you want to do but it definitely sets the window
>> > location
>> > (url) to something that when you hit refresh gives you the same page
>> you
>> > see
>> > before you hit refresh (with updated server content, if anything there
>> has
>> > changed in the interim).
>> >
>> >
>> >
>> >
>> >
>> >
>> > Siemen Baader wrote
>> >> Hi,
>> >>
>> >> Not really answering your questions, but I think this is what Iliad is
>> >> for. There you have access to a restful URL on every (xhr) request and
>> >> every WAComponent is transparently updated via xhr, including when you
>> >> use
>> >> call-answer.
>> >>
>> >> Just FYI. :)
>> >>
>> >> Siemen
>> >>
>> >> Sent from my iPhone
>> >>
>> >>> On 12 Apr 2019, at 17.23, Johan Brichau <
>> >
>> >> johan@
>> >
>> >> > wrote:
>> >>>
>> >>> Hi Esteban,
>> >>>
>> >>> I’m interested in this too.
>> >>> We have the same kind of application (I guess… using mostly Ajax
>> >>> updates,
>> >>> I mean) but I have just been postponing a retry to address the
>> >>> inconsistency with ajax, server state and the back button for years….
>> >>>
>> >>> Within a callback, you can get at the #actionUrl on the renderer.
>> >>> Is that what you want?
>> >>>
>> >>> I’m trying to dive into what would be required here… trying to
>> >>> understand
>> >>> what you need:
>> >>> Ajax requests always update the same continuation state in Seaside
>> (i.e.
>> >>> they do not create a new continuation)… so if you push that url to
>> the
>> >>> history after each ajax update, the back button will essentially
>> request
>> >>> the same but updated continuation from Seaside, meaning you should
>> see
>> >>> the state as it is in on the server, and not how it was before you
>> made
>> >>> the ajax request (which is what happens now).
>> >>>
>> >>> Does that correspond to what you are trying to fix?
>> >>>
>> >>> Johan
>> >>>
>> >>>> On 12 Apr 2019, at 15:17, Esteban Maringolo <
>> >
>> >> emaringolo@
>> >
>> >> > wrote:
>> >>>>
>> >>>> I have a Seaside application that is almost 100% AJAX driven to
>> >>>> replace the visual components (no call/answer involved).
>> >>>>
>> >>>> I plan to to use history.pushState() and history.replaceState()
>> >>>>
>> >>>> My plan is to update the browser url after replacing some
>> components,
>> >>>> but using the URL building of the existing #updateUrl: (including
>> >>>> _s/_k parameters and whatnot).
>> >>>>
>> >>>> Is it possible to obtain the URL of the session presenter within the
>> >>>> ajax response?
>> >>>>
>> >>>> Thanks in advance,
>> >>>>
>> >>>> Esteban A. Maringolo
>> >>>> _______________________________________________
>> >>>> seaside mailing list
>> >>>>
>> >
>> >> seaside at .squeakfoundation
>> >
>> >>>> http://lists.squeakfoundation.org/cgi-bin/mailman/listinfo/seaside
>> >>>
>> >>> _______________________________________________
>> >>> seaside mailing list
>> >>>
>> >
>> >> seaside at .squeakfoundation
>> >
>> >>> http://lists.squeakfoundation.org/cgi-bin/mailman/listinfo/seaside
>> >> _______________________________________________
>> >> seaside mailing list
>> >
>> >> seaside at .squeakfoundation
>> >
>> >> http://lists.squeakfoundation.org/cgi-bin/mailman/listinfo/seaside
>> >
>> >
>> >
>> >
>> >
>> > --
>> > Sent from: http://forum.world.st/Seaside-General-f86180.html
>> > _______________________________________________
>> > seaside mailing list
>>
>> > seaside at .squeakfoundation
>>
>> > http://lists.squeakfoundation.org/cgi-bin/mailman/listinfo/seaside
>>
>>
>>
>>
>>
>> --
>> Sent from: http://forum.world.st/Seaside-General-f86180.html
>> _______________________________________________
>> seaside mailing list
>> 

> seaside at .squeakfoundation

>> http://lists.squeakfoundation.org/cgi-bin/mailman/listinfo/seaside
> _______________________________________________
> seaside mailing list

> seaside at .squeakfoundation

> http://lists.squeakfoundation.org/cgi-bin/mailman/listinfo/seaside





--
Sent from: http://forum.world.st/Seaside-General-f86180.html


More information about the seaside mailing list