0

So my PHP webiste generates DHTML output that looks like the following:

<div class="toggle-ctrl" onclick="toggleMenu();">
    click me to toggle menu
</div>
<div id="site-menu">
    <ul>
        <li>opt 1</li>
        <li>opt 2</li>
    </ul>
</div>
<p><a href="#">Link to Myself</a></p>

And of course, when clicked, the first div calls some JavaScript which toggles the visibility of the site-menu

function toggleMenu() {
    var navigation_pane  = document.getElementById('site-menu').style;
    if ( navigation_pane.display == 'none' )
        navigation_pane.display = 'block';
    else
        navigation_pane.display = 'none';
}

All this works fine. It's clicking on the link which is bothering me right now. Clicking it (of course) creates a new http request, and my PHP engine re-generates the page again.

The problem occurs when the visibility of the site-menu is 'none'. The PHP engine doesn't know that the menu is hidden, so it generates the same-html again, and the browser places the menu back in front of the surprised-looking user.

The question therefore, is how do I inform PHP (or how can PHP go to check) what the status of the site-menu's visibility is, before it goes to re-generate the page?

cartbeforehorse
  • 3,045
  • 1
  • 34
  • 49

5 Answers5

2

The question therefore, is how do I inform PHP (or how can PHP go to check) what the status of the site-menu's visibility is, before it goes to re-generate the page?

It can't. By the time the HTML is delivered to the browser, PHP is no longer in the picture. The only way you could make PHP aware of this would be to send a parameter in the URL indicating the menu is hidden, or set a cookie and have the cookie indicate visibility of the object. Then PHP can check for the presence of this value and set the visibility of the div when it renders it.

You could accomplish this in a number of ways, for example:

  • Use document.cookie to set the cookie in your toggleMenu function.
  • Use ajax to notify PHP in the toggleMenu function and have PHP set a cookie or session value
  • Append a flag to the link indicating the visibility of the menu from the toggleMenu function.
drew010
  • 68,777
  • 11
  • 134
  • 162
  • I think you're right - cookies is the way forward. Appending this quality of information to the link is messy, and abuses the clean method by which PHP generates the link in the first place. Using $_POST also adds to the undidyness on the required HTML and JavaScript. >> By contrast, I really need to look into the AJAX thing; I don't know enough about it. Thanks. – cartbeforehorse Apr 25 '12 at 19:16
  • Yeah Ajax is another good possibility. Basically when they toggle the div, you sent an http request behind the scenes to your server which would then set the cookie or update the session. JavaScript settings cookies is okay too, but Ajax may be easier to manage in the long term. Libraries like jQuery or Prototype can make Ajax requests very simple to make. – drew010 Apr 25 '12 at 21:00
2

There are at least two options other than sending the menu state to the PHP script.

  1. Use AJAX to load just part of the page. If you don't reload the menu, you don't need to re-initialize its style. Before going down this path, examine whether AJAX is suitable. If you implement this solution, don't break browser functionality.
  2. Modern browsers support a storage mechanism. Store the menu state in localStorage when it changes, and set the menu state when the page loads. To support older browsers, you can create an API that uses web storage when available and cookies when not (jQuery.Storage does this).

    Menu.js:

    /* implementation of Storage, Class and addEventListenerTo left as 
       an exercise for the reader.
    */
    var Menu = {
        init: function(id, toggleId) {
            if (! toggleId) {
                toggleId = id + '-toggle';
            }
            var toggler = document.getElementById(toggleId),
                menu = document.getElementById(id);
            menu.toggler = toggler;
            /* addEventListenerTo should call the browser-supplied event subscriber 
               method (e.g. addEventListener or attachEvent)
            */
            addEventListenerTo(toggler, 'click', 
                function(evt) {
                    Menu.toggle(id);
                });
    
            if (! Storage.exists(id+'-open')) {
                Storage.set(id+'-open', true);
            }
    
            if (Storage.get(id+'-open')) {
                Menu.open(id);
            } else {
                Menu.close(id);
            }
        },
        toggle: function(id) {
            var menu = document.getElementById(id);
            Class.toggle(menu, 'open closed');
            if (Class.has(menu, 'open')) {
                menu.toggler.firstChild.nodeValue = 'close menu';
                Storage.set(id + '-open', true);
            } else {
                menu.toggler.firstChild.nodeValue = 'open menu';
                Storage.set(id + '-open', false);
            }
        },
        setState: function (id, toAdd, toRemove) {
            var menu = document.getElementById(id);
            Class.remove(menu, toRemove);
            Class.add(menu, toAdd);
        },
        open: function(id) {
            this.setState(id, 'open', 'closed');
        },
        close: function(id) {
            this.setState(id, 'closed', 'open');
        }
    };
    

    some CSS file:

    .closed { display: none; }
    

    page:

    <div id="site-menu-toggle" class="toggle-ctrl">close menu</div>
    <div id="site-menu" class="open">
        <ul>
            <li>opt 1</li>
            <li>opt 2</li>
        </ul>
    </div>
    <p><a href="#">Link to Myself</a></p>
    
    <script type="text/javascript">
    Menu.init('site-menu');
    </script>
    

    You can play with a live version of the Menu.js approach on jsFiddle. Using jQuery, you can do away with Menu.js, resulting in a much more succinct implementation:

    <script type="text/javascript">
    $('#site-menu-toggle').click(function (evt) {
            var $menu = $('#site-menu');
            $menu.toggleClass('open close');
            $.Storage.set('site-menu-state', $menu.attr('class'));
            if ($menu.hasClass('open')) {
                $('#site-menu-toggle').text('close menu');
            } else {
                $('#site-menu-toggle').text('open menu');
            }
        });
    $(function() {
        var state = $.Storage.get('site-menu-state');
        if (! state) {
            $.Storage.set('site-menu-state', $('#site-menu').attr('class'));
        } else {
            $('#site-menu').attr('class', state);
        }
    });
    </script>
    

    There's a jFiddle for the jQuery menu state implementation that you can play with.

Since differences in the menu state don't conceptually make for different resources, it doesn't matter whether having the menu open or closed is bookmarkable or affected by history.

NB. don't use the text "click me", it's too verbose and redundant (what other action is there? Affordances should be implicit.). Instead, you can use a graphic to indicate open/close, or simply say "open menu"/"close menu".

Community
  • 1
  • 1
outis
  • 75,655
  • 22
  • 151
  • 221
  • Hmmm... Seems a bit sledgehammer-to-crack-an-egg. You're definitely a more experienced web programmer than I am, but I'll stick to something simpler thanks. And thanks for the innocent hint at the end - but of course, my example is intentionally simplified for illustrative purposes. My list options aren't really called 'opt1' and 'opt2' either ;-) – cartbeforehorse Apr 25 '12 at 20:16
  • 1
    @cartbeforehorse: I hope the main thing you take away from this answer are the possibilities, which someday may solve a different problem you face. I'd also like to point out the use of classes to handle hiding the menu instead of modifying the "display" property directly. Using classes, you can easily extend toggling to non-block elements, though you can also set the display property to an empty string to show elements of any display type. You can use classes for additional styling affects; for example, `.open:before` and `.closed:before` lets you set markers for open and closed menus. – outis Apr 25 '12 at 21:43
  • Indeed. I most definitely understand the potential of what you're suggesting, and I didn't mean to give the impression that I don't appreciate your answer. The subtleties of JavaScript are something that I'm aware of by association, but being a "back-end" guy, I'm wary of getting my hands dirty in the "flashy presentation" stuff, and distracting myself from the main project goals. When my project reaches the refinement stages, I'll probably come back and use your answer as a reference to how I can improve the presentation layer. – cartbeforehorse Apr 26 '12 at 08:32
  • My requirements are advancing somewhat. In posting this question, my animation requirements were "nice-to-have" only, and I didn't want to get into the detail of learning about jQuery. But now that I have a fairly solid underlying application up and running, the "wouldn't it be nice if..." requests are coming in, and I'm being forced down the AJAX route, which is also forcing my hand on jQuery. I've yet to learn to love its syntax, but am seeing why y'all keep harping on about it so much. Point is, I've taken a lot out of this answer in the longer term. So thanks again. – cartbeforehorse Jun 07 '12 at 17:47
  • @cartbeforehorse: in case you haven't seen the term before, [jQuery](http://stackoverflow.com/q/3398930/) uses a [fluent interface](http://martinfowler.com/bliki/FluentInterface.html). Hopefully, that concept makes jQuery's interface more sensible to you. – outis Jun 07 '12 at 18:05
  • Of course, what one starts to notice after a bit more messing about with jQuery, is that my original question question becomes irrelevant. PHP doesn't have to worry about setting the visibility of the menu at all. It can send the page layout in a static format, and let jQuery worry about setting the menu's visibility according to what state is set in the user's browser. We don't even have to mess about with CSS or flipping the class to which the belongs. This is a hugely clean way of keeping presentation separate from the business logic. – cartbeforehorse Jun 15 '12 at 15:38
1

In addition to drew010's suggestions: You could also create a form with a hidden input element named, let's say, 'menu_status' whose value gets set by toggleMenu(). Then when you click on your link, use javascript to POST or GET the form. Then you read the value server-side with php using either $_POST["menu_status"] or $_GET["menu_status"], depending on the form method.

UPDATE: Something like this:

<form name="session_form" action="" method="POST">
    <input type="hidden" name="menu_state" value="block">
</form>

<?php $menu_state = isset($_POST["menu_state"]) ? $_POST["menu_state"] : "block"; ?>

<div id="site-menu" style="display:<?php echo $menu_state; ?>">
<ul>
    <li>opt 1</li>
    <li>opt 2</li>
</ul>
</div>

<p><a href="#" onClick="document.forms.session_form.submit();return false;">Link to Myself</a></p>

function toggleMenu() {
    var navigation_pane  = document.getElementById('site-menu').style;
    if ( navigation_pane.display == 'none' )
        navigation_pane.display = 'block';
    else
        navigation_pane.display = 'none';
    document.forms.session_form.menu_state.value = navigation_pane.display;
}

EDIT: Using jQuery ajax could involve something like this:

<div class="toggle-ctrl">click me to toggle menu</div>

<?php $menu_state = isset($_POST["menu_state"]) ? $_POST["menu_state"] : "block"; ?>

<div id="site-menu" style="display:<?php echo $menu_state; ?>">
<ul>
    <li>opt 1</li>
    <li>opt 2</li>
</ul>
</div>

<p><a href="#" id="go">Link to Myself</a></p>

$("div.toggle-ctrl").click(function(){
    $("#site-menu").toggle();
});

$("#go").click(function(e) {
    e.preventDefault();
    var menu_state = $("#site-menu").css("display");
    $.post("", {menu_state:menu_state}, function (response) {
        $("html").html(response);
    });
});

Or without using ajax or a form, just append a parameter to the link and use $_GET instead of $_POST in your php:

$("#go").click(function(e) {
    e.preventDefault();
    var menu_state = $("#site-menu").css("display");
    document.location.href = "index.php?menu_state=" + menu_state;
});

This seems to me the simplest solution.

Stefan
  • 3,850
  • 2
  • 26
  • 39
  • Bonus points for including the code as well!! That's really much more compact than I thought possible. I thought drew010 had taken it with his cookie suggestion, but he may have been pipped at the $_POST[] – cartbeforehorse Apr 25 '12 at 19:31
  • @cartbeforehorse: Thanks. If you use the last solution, remember to load jQuery and also to give your link an id ('go' in my example). If you don't want to use jQuery just create an onClick javascript function for the link, which sets document.location – Stefan Apr 25 '12 at 19:34
  • Everyone seems to be touting this jQuery library, and I kind of see where you're all going with it from your examples. But beneath it all, and to my untrained eye, it just appears to complicate your original beautifully elegant solution. – cartbeforehorse Apr 25 '12 at 20:45
  • @cartbeforehorse: Well, using jQuery very often results in huge simplifications and elegance. For example your entire toggleMenu() can be replaced by `$("div.toggle-ctrl").click(function(){$("#site-menu").toggle();});` But sometimes 'normal' javascript is fine. Another possibility is to create the form dynamically only when it is needed: http://stackoverflow.com/questions/7013109/submit-is-not-a-function-error-in-firefox-in-dynamically-created-form-without – Stefan Apr 25 '12 at 21:24
1

Actually, there are several types of answers to your question.

While it may sound there's no way to do what you want, there are, in fact, many ways.

Cookies

The obvious. Cookies can be accessed by javascript as well as PHP. Just modify the cookie whenever the menu is shown/hidden through javascript (there's the excellent jQuery cookie plugin).

Form input

If you are submitting a form, simply have a hidden input keep the value of the menu's visibility:

<input type="hidden" name="menu-visibility" value="0"/>

Again, you need javascript to keep this input updated.

Update relevant parts of the page

This is the hip & leet new trend. Well, actually, it's been there for some 6 years or so. Basically, don't submit anything and don't reload the page. Update the parts of the page that actually need updating, through AJAX.

Local Storage

As @outis mentioned, today browsers have something similar to cookies, except they keep it for themselves (hence locally). It's a pretty new feature, to be honest, I wouldn't trust it considering there are better ways to accomplish what you need.

Christian
  • 27,509
  • 17
  • 111
  • 155
  • Hidden behind my question... there are reasons that I have to go back to the server on a user-click (i.e. to fetch some data from my database). For this reason, it sounds as though AJAX won't do what I want (although I don't fully know what the limitations of AJAX are). I never heard of local-storage before, but thanks for your tips on it :-) – cartbeforehorse Apr 25 '12 at 20:26
  • 2
    You definitely _can_ use AJAX to fetch data from your database... that's the X in "AJAX," your data coming back as XML, or JSON, or whatever. Then you update just what needs updating. – ben author Apr 25 '12 at 21:39
  • AJAX. Sounds like my next purchase from Amazon. – cartbeforehorse Apr 26 '12 at 08:40
  • Regardless what it sounds like, it's been driving a good part of the internet these last few years... – Christian Apr 26 '12 at 12:59
0

I know it's not cool to answer your own question, but another possible solution occurred to me last night, and it only requires 1 new line of code to be written (sort of).

The first part of the solution has already been implicitly suggested by many of you. Modify the JavaScript to write to a cookie:

function toggleMenu() {
    var navigation_pane  = document.getElementById('site-menu').style;
    if ( navigation_pane.display == 'none' )
        navigation_pane.display = 'block';
    else
        navigation_pane.display = 'none';
    document.cookie = "menu_vis=" + navigation_pane.display; // +1 line of code
}

Now, what are the possibilities if your CSS file just so happens to be a PHP file in disguise? my_css.php would look something like this:

<?php
header("Content-type: text/css");
?>
#site-menu {
    display: <?php echo isset($_COOKIE['menu_vis']) ? $_COOKIE['menu_vis'] : 'block'; ?>;  /* line of code modified, but not added! */
}

Tested this morning, and it works.

I find it a neat solution, because it means that I don't have to bend my PHP or HTML design around any presentational concerns.

-- I appreciate that there are more "encompassing" solutions out there. If I was a better JavaScript developer, (or made use of jQuery or the like), I could build more complicated classes which could then be applied more generally to other HTML elements. I may come back to investigate such solutions later, but that's just not where my project is at the moment.

Thank you everyone for all your replies. I wouldn't have found this solution without bouncing these ideas off you guys.

cartbeforehorse
  • 3,045
  • 1
  • 34
  • 49
  • Just be careful about injection attacks. Domain restrictions on cookies should make this difficult, and taking advantage of exploiting a CSS vulnerability may be difficult (I have vague visions of XSRF using `background-image`), but neither means you can afford to be complacent. Besides, it's a simple task to sanitize the value for a `display` property. – outis Apr 29 '12 at 04:58
  • You can implement [clean URLs](http://stackoverflow.com/q/183921/) to hide the fact that the CSS is actually generated by PHP. Give the script an extension of ".css.php" and use standard techniques to hide the ".php" file extension in URLs. Make sure your PHP script sets the "Content-type" header. Also, be careful about caching. Append a randomly generated value to the query string, or send cache-disabling headers from the script, or switch to a query string parameter instead of a cookie to pass the menu state. – outis Apr 29 '12 at 05:15
  • 1
    One nice thing about using cookies to hold the menu state is you can easily switch among the different ways of setting the menu state, since cookies are accessible to JS and PHP scripts alike. – outis Apr 29 '12 at 05:19
  • @outis You're right. The correct answer to my original question is simply "cookies". Both PHP and JavaScript can access the same cookies, so what each of the engines does with them after that, is open to the programmer's imagination. – cartbeforehorse Jun 20 '12 at 16:39