backdrop-contrib / layout_wildcard

GNU General Public License v2.0
0 stars 2 forks source link

Roadmap for Layout Wildcard evolution #19

Open bugfolder opened 3 years ago

bugfolder commented 3 years ago

This issue is devoted to discussion about the roadmap for Layout Wildcard module.

The Layout Wildcard module allows individual layouts to be used on multiple paths.

This ability already exists to some degree in default Backdrop via the use of placeholders (%) in a layout's path. A layout will be applied to any path in the menu router table that matches the layout's path and that has the same placholders. Layout Wildcard module expands the range of paths on which any particular layout can be applied.

In subsequent comments, I'll describe:

  1. The core Backdrop treatment of layout paths;
  2. The current behavior of Layout Wildcard module;
  3. Some proposed changes to Layout Wildcard module.
bugfolder commented 3 years ago

Core Backdrop Behavior

[Updated 2021-12-18 to better reflect current Backdrop terminology for the various types of paths.]

To explain what Layout Wildcard provides, it will be helpful to review the default behavior of layout paths.

Some links relevant to layouts:

For our purposes, the main concepts to note are the concept of a layout path and the concept of placeholders. There are two types of layouts. The User Guide to Layouts and Templates refers to them as "stand-alone" layouts and "dynamic" layouts, stating that

a stand-alone layout will only ever affect a single page on the site. The layout itself generates this page. `A good example of a stand-alone layout is the default Home page.

a dynamic layout is a layout that can affect multiple pages. In this case, the pages that are affected are always generated by other sources. This type of layout will require the "Main page content" block.

For consistency, we will continue using these terms, although with the Layout Wildcard module, the distinction gets a bit blurred, as we will see.

The distinction between the two comes down to what's in the layout's path:

For both types of path,

So, both types of layouts can create pages that don't yet exist and customize existing pages.

Note that there is a temporal order dependence to the behavior of Layouts with respect to existing page paths. See issue 4986.

Stand-alone layouts can be used to create a one-of-a-kind page with a special layout (like, for example, the home page, which is one of the out-of-the-box layouts provided in the default Backdrop installation).

They can also be used to customize the layout for a single existing page, such as one provided by a module via hook_menu().

Dynamic layouts can be used to create a set of related pages that display different information, with the differences provided by the path-specific values in the placeholders, which can then be passed to the blocks that make up the page via contexts (see more below).

They can also be used to customize the layout of an existing set of pages, however, there needs to be a path already in existence that contains the same placeholders as the layout path.

That's where things get a bit complicated; the complication stems from the nature of placeholders and some ambiguity around the term "path".

In Backdrop, there are several things that can be referred to as a path and confusion can arise if we interpret "path" the wrong way:

In Layouts, the path that is defined for the layout is a router path.

Placeholders (%) behave in some ways like the wildcard asterisk (*) that you may be familiar with from Block visibility conditions (and are also used in Layout visibility conditions). But they are distinctly different animals, both more limited and more powerful than simple wildcards:

Placeholders in layout paths are more powerful than mere wildcards because they can define a context, a chunk of related information, that can be passed to the blocks in the layout. For example, in a path node/%/something, the % can be interpreted as the node ID of a node (via the context mechanism), which results in passing the referenced node to the block as context for the block. (The context can also be passed to the block as simply the text string of the placeholder, or several other things.)

Importantly, though, dynamic layouts—layout with paths containing placeholders—are only applied to pages that already exist in the menu router table whose router paths were defined to include placeholders.

An example might help to make this distinction clear. Suppose you created a View that had two displays: one with path page/1, and one with path page/2. It might seem plausible that a layout with path page/% would be applied to both. But it would not, because there is no menu router entry for /page/%, only for the two specific paths page/1 and page/2 created by the View.

On the other hand, you could create a View with a contextual filter that had the path page/%, and in this case, the layout would be applied to both page/1 and page/2, because now the router path page/% exists, created by that View.

It would be nice, of course, if the first case—a View with multiple displays whose paths are similar, but defined explicitly—could be handled with the same Layout. The Layout Wildcard module provides a way to do that.

bugfolder commented 3 years ago

Layout Wildcard (current behavior as of v1.x-1.2)

When you enable Layout Wildcard module, two new things happen:

Ancestor Matching

This was added to Layout Wildcard in version 1.x-1.1; see backdrop-issues/issues/2412.

Ordinarily, a layout with a placeholder path would be applied to a requested page if

When Layout Wildcard module is enabled, a layout with a placeholder path may be applied to a page if

Here we need to introduce the notion of a path's ancestors. The ancestors of a path are obtained by successively lopping off text delimited by /'s and/or replacing said text with placeholders.

An example is given in the documentation for menu_get_ancestors(). For the path node/12345/edit, for example, the ancestors are:

Ancestor matching handily addresses the View situation described above. If you create a View with displays at page/1 and page/2, and create a layout with path page/%, with ancestor matching, it may be applied to both pages. The ancestors of page/1 are

And because there is a match on page/%, the layout would be applied. Same for page/2.

However, there is an important caveat of ancestor matching: every layout with a path that matches any ancestor of the requested page path will be eligible to be applied for a given page page (the one that is actually selected depends on the order of the layouts on the Layouts configuration page and access checks from, e.g., visibility conditions).

So, for example, if you've created a layout with the path page and another with the path page/subpage/% and then create a View with a display path page/subpage/1, both layouts will be eligible to display the View page:

You'll need to make sure your Views are ordered so that the more specific layout paths come before the more generic ones in order to get the desired behavior.

Alternative Paths

The Layout Wildcard module also adds a new field, "Alternative Paths", to each layout's configuration page. In this field, you can add additional paths, with or without placeholders, that the given layout should be used on.

Thus, for example, in the case of the View with two displays, you could add the specific paths page/1 and page/2 as alternative paths for a layout with path page, and then this layout would be used for those two displays.

The nice thing about alternative paths is that they can be precisely targeted to specific paths. You can create one layout that targets page/1 and page/3, and another that targets page/2 and page/4, which would be problematic using ancestor path matching.

However, there is an important limitation when using alternative paths that relates to contexts. When a layout constructs the contexts to pass to its blocks, it uses the main layout path to decide what contexts are needed and built. (See call to layout_context_required_by_path() in Layout::getContexts().)

Most of the time, this will do the right thing. If you use paths page/1 and page/2 as alternative paths for a layout with primary path page/%, then the 1 or 2 (as appropriate) will be interpreted as contextual parameters that will be passed to the layout. So we can achieve our goal like this:

And then if you visit page/1, it will display the content from the View with display page/1, using the layout with path page/% and using 1 to derive the context to be passed to its blocks. Similarly for page/2.

(Note: at present, there seems to be a bug in Layout that limits what types of contexts you can set for paths with placeholders.) [Edit 2021-09-12: this bug is now fixed.]

Note, though, that by creating a layout with path page/%, you've now created an entry page/% in the menu routing table, which means you can pass anything for that third parameter and you will still get the page generated by the layout, but not the View. So the path page/3 would result in a page generated by the layout that is empty of content. It would still have its blocks, of course, which could respond to any context that was generated from the 3 parameter. But it would not return a "Page not found" error, which would have been the case had we not created the layout with a placeholder path.

We could have instead created the Layout with path page with no placeholders but with the two alternative paths. If we did that, then page/1 and page/2 would return the proper pages (Views display pages wrapped in the layout), and page/3 would return a 404. But in this case, there would be no context associated with the 1 or 2 passed to the blocks of the page. And note that a request path page would still return an empty page, rather than a 404 error.

Note that Backdrop gracefully handles the situation where a Layout is applied to a path that doesn't have anything for the placeholder. For example, this scenario:

In this case, the user will see the content from the module page bar, wrapped in the layout with path foo/%, but since the path has no data in position 2, there will be no context generated for this path that is passed to the layout's blocks.

[Edit 2021-09-12: Layout Wildcard 1.x-2.0+ strictly enforces the required number of placeholders in alternative paths.]

What about URL Aliases?

Another limitation of the Layout Wildcard module (and the Backdrop Layout system in general) is that all the path comparison/decision logic depends on the normal path (from which the router path is extracted for use in comparisons). Many pages will have URL aliases defined for them. While the aliases can be used in Layout and Block visibility conditions, only the system or normal path is used to determine which layout is applicable for a given requested path. So URL aliases play no role in any of the path matching rules described above.

This is a little odd, because URL aliases can be used in visibility conditions that are part of contexts attached to layouts. It would be nice if they could be used in layout path matching directly.

bugfolder commented 3 years ago

Possible changes

I see three significant issues that warrant addressing:

I haven't discussed the issue of contexts yet. Layouts that contain placeholders in their path can also have contexts for the item that fills in the placeholder. Any treatment that substitutes one layout for one with a different path will need to ensure that contexts are handled properly for the substituted layout.

bugfolder commented 3 years ago

See issue Changes coming in Backdrop 1.20 for details on Layout Wildcard 2.0+. In brief:

bugfolder commented 3 years ago

URL Aliases

In visibility conditions, we can specify that a block's appearance depends on the "URL alias" (which is equivalent to the "request path" in the comment above; it's the path that is typed into the browser, what is returned by the system function request_path()). In visibility conditions, we can use * as a wildcard, so that, for example, fooba* would match both foobar and foobaz.

It would be nice to be able to allow path matching that behaves like the path matching in visibility conditions, which, in particular, allows matching to the URL alias (if a URL alias exists). If no alias exists, then it will consider matching to the normal path.

Here's what that dialog looks like:

visibility condition

This suggests that one way of adding this functionality to LW would be to add a second field like Alternative paths, titled (perhaps) URL paths that permits paths to be entered in exactly the same way as the visibility condition does, with wildcards.

The logic would then go as follows:

For each layout:

If so, add this layout to the list of additional layouts, performing the repositioning of contexts if needed (as with ancestor matching and alternative paths).

Question: should this just replace the Alternative paths field? That is, if we had both, is there anything that could be accomplished by having an entry in Alternative paths that couldn't equally be accomplished by an entry in URL paths?