WireMarkupRegions

Implements the Markup Regions output strategy, where template files inject fragments of markup into named regions within a shared layout file

Enable in /site/config.php:

$config->useMarkupRegions = true;

Once enabled, any HTML output from a template file that appears before the first <!DOCTYPE> declaration is treated as a set of region updates rather than direct output. ProcessWire matches each element to a region in the layout file by id, pw-id, or data-pw-id, then applies the specified action.

Template file usage (the common case)

The Markup Regions system is driven by pw-* attributes in your template output. No PHP calls to WireMarkupRegions are needed — you just output HTML with these attributes and ProcessWire handles the rest.

Actions via pw-* attributes

<!-- Replace the #content region entirely -->
<div pw-replace="content"><p>New content here</p></div>
<!-- or: match by id attribute on the element itself -->
<div id="content" pw-replace><p>New content</p></div>

<!-- Append to #content -->
<p pw-append="content">This paragraph gets appended.</p>

<!-- Prepend to #content -->
<p pw-prepend="content">This paragraph gets prepended.</p>

<!-- Insert before the #sidebar element -->
<aside pw-before="sidebar">Goes before sidebar</aside>

<!-- Insert after the #sidebar element -->
<aside pw-after="sidebar">Goes after sidebar</aside>

<!-- Remove the #promo region entirely -->
<div pw-remove="promo"></div>

Merging attributes onto the target

When you include other attributes on a region element, they are merged onto the target element in the layout — not just applied to your inserted content:

<!-- Append content AND add class "has-sidebar" to #main in the layout -->
<div id="main" class="has-sidebar" pw-append>
    <aside>Sidebar content</aside>
</div>

<!-- Append content AND remove class "no-js" from #body in the layout -->
<div id="body" class="-no-js" pw-append></div>

Prefix a class name with - to remove it from the target.

Optional regions

Mark a region with pw-optional (or data-pw-optional) to have it automatically removed from the final output if it ends up empty:

<div id="promo" pw-optional>
    <?php if($page->promo_text): ?>
        <p><?= $page->promo_text ?></p>
    <?php endif ?>
</div>
Programmatic usage

WireMarkupRegions can also be used directly for general-purpose HTML manipulation, independent of the template system.

$regions = new WireMarkupRegions();

find($selector, $markup, $options)

Locate elements in an HTML document matching a selector.

$results = $regions->find('#content', $html);
$results = $regions->find('.sidebar', $html);
$results = $regions->find('<footer>', $html);       // by tag name
$results = $regions->find('data-region=main', $html); // by attribute value
$results = $regions->find('[pw-action]', $html);    // any pw-* action attribute

Selector formats:

SelectorMatches
#nameid, pw-id, or data-pw-id equals "name"
.nameclass attribute contains "name"
.name*class attribute starts with "name" (prefix wildcard)
tag.nameSpecific tag with class "name"
<tag>All instances of a specific HTML tag
attribute=valueAttribute with exact value
tag[attribute=value]Specific tag with that attribute value
[pw-action]Any element with a pw-* action attribute
attributeElement with attribute present (any value)

Options:

OptionDefaultDescription
singlefalseReturn only the first match's markup string (not array)
verbosefalseReturn detailed info array per region
wrapnullInclude wrapping tags (auto: true for class, false for id)
max500Maximum regions to find
exactfalseReturn region markup exactly as-is
leftoverfalseInclude a 'leftover' key with unmatched markup

Return shapes:

  • Default: array of matched region strings.
  • single=true: first matched region string, or an empty string when not found.
  • verbose=true: array of region info arrays with keys such as name, pwid, open, close, attrs, classes, action, actionTarget, region, and html.
  • leftover=true: includes a leftover array entry containing markup not consumed by matched regions.

update($selector, $content, $markup, $options)

Update matching regions with content using a specified action.

$result = $regions->update('#main', '<p>New content</p>', $html, ['action' => 'replace']);
$result = $regions->update('.sidebar', '<p>Extra</p>', $html, ['action' => 'append']);

Actions: replace, update, append, prepend, before, after, remove, auto.

Convenience wrappers call update() with the matching action:

$html = $regions->replace('#main', '<p>New</p>', $html);
$html = $regions->append('#main', '<p>After existing content</p>', $html);
$html = $regions->prepend('#main', '<p>Before existing content</p>', $html);
$html = $regions->before('#main', '<aside>Before element</aside>', $html);
$html = $regions->after('#main', '<aside>After element</aside>', $html);
$html = $regions->remove('#promo', $html);

populate(&$htmlDocument, $htmlRegions, $options)

The primary internal method — matches template output to layout regions and applies all pending updates. Called automatically by ProcessWire; you rarely need to call this directly.

$numUpdates = $regions->populate($mainHtml, $templateOutput);
Tag utilities

mergeTags($htmlTag, $mergeTag)

Merge attributes from one HTML tag string into another. The tag name from $htmlTag is kept; class attributes are merged (not replaced); other attributes in $mergeTag are added or overwrite those in $htmlTag.

$tag = $regions->mergeTags(
    '<div id="main" class="old">',
    '<div class="new extra" title="hello">'
);
// '<div id="main" class="old new extra" title="hello">'

Class merge operators:

// Add a class (force-add even if previously removed)
$regions->mergeTags($tag, '<div class="+highlight">');
// Remove a class
$regions->mergeTags($tag, '<div class="-old-class">');
// Remove all classes matching a prefix
$regions->mergeTags($tag, '<div class="-col-*">');

getTagInfo($tag)

Parse an HTML opening tag and return an info array including name, id, pwid, classes, attrs, action, actionTarget, and close.

$info = $regions->getTagInfo('<div id="main" class="wrap" pw-append>');
// ['name' => 'div', 'id' => 'main', 'action' => 'append', 'actionTarget' => 'main', ...]

renderAttributes($attrs, $encode, $quote)

Render an associative array as an HTML attribute string. Boolean true values render as standalone attributes (e.g. checked); array values are joined with spaces.

$str = $regions->renderAttributes(['id' => 'main', 'class' => 'wrap', 'checked' => true]);
// 'id="main" class="wrap" checked'

hasAttribute($name, $value, &$html)

Check whether an HTML attribute appears anywhere in the given markup.

if($regions->hasAttribute('id', 'main', $html)) { ... }
if($regions->hasAttribute('class', 'sidebar', $html)) { ... }

removeRegionTags(&$html)

Remove <region> and <pw-region> wrapper tags from markup and strip pw-id / data-pw-id attributes from remaining tags. Returns true when the markup changed.

if($regions->removeRegionTags($html)) {
    // $html was modified in place
}

hasRegions(&$html) / hasRegionActions(&$html)

Fast checks for markup that contains region identifiers or pw-* actions.

if($regions->hasRegions($html)) { ... }
if($regions->hasRegionActions($html)) { ... }
Stripping

stripRegions($tag, $markup, $getRegions)

Strip non-nested tags (comments, scripts, styles) from markup, or extract them.

$clean    = $regions->stripRegions('<!--', $markup);      // remove HTML comments
$clean    = $regions->stripRegions('<script', $markup);   // remove script tags
$stripped = $regions->stripRegions('<!--', $markup, true); // extract comments instead

stripOptional($markup)

Remove elements with pw-optional / data-pw-optional that are empty; strip the attribute from non-empty ones.

$clean = $regions->stripOptional($markup);
Notes
  • Source file: wire/core/Tools/WireMarkupRegions/WireMarkupRegions.php
  • Enable with $config->useMarkupRegions = true in /site/config.php.
  • Template output before <!DOCTYPE> is treated as region updates; output after is the layout document.
  • pw-id and data-pw-id attributes work like id for region targeting, without affecting the rendered DOM id.
  • All pw-* action attributes are stripped from the final output.
  • <!--#name--> HTML comments serve as fast-match hints for closing tags, improving parsing performance on large documents.
  • Single-use tags (<html>, <head>, <body>, <title>, <main>, <base>) can be targeted by tag name alone without an id.
  • The ^ prefix on an action target (pw-append="^footer") matches by tag name rather than id.
API reference: methods

File regions are an experimental part of ProcessWire’s Markup Regions output strategy. File regions enable you to define CSS, JS, SCSS or LESS in your markup alongside the markup that is styled or manipulated by it, keeping everything together as a single component, in cases where it's worthwhile. Unlike inline styles or scripts, ProcessWire takes care of moving these assets to one or more external asset files. These external asset files are automatically updated whenever you make a change to your file regions.

File regions require ProcessWire 3.0.254 or newer. They are not enabled by default. Markup Regions with File Regions are enabled with $config->useMarkupRegions = 2; in your /site/config.php file. More about: Markup Regions.

How to use file regions for CSS:

Add a <link> tag like the following in the HTML <head> section:

<link rel="stylesheet" href="main.css">

Now you can populate the file linked above from one or more file regions, anywhere in your output using a <style> tag. In the <style> tag, you must specify a "pw-file" attribute, and either an "id" or "pw-id" attribute containing a unique ID for your file region. This unique ID is how ProcessWire will keep track of it and know when to update it.

<style id="hello-world" pw-file="main.css">
   .hello-world {
     color: red;
   }
</style>

That's all that is necessary. Any time you make a change to the region above, it will be automatically updated in the main.css file. Note that the <style> tags do not appear in the final document output, as their contents is moved to the main.css file.

Now let's add another region to the main.css file, from somewhere else in our output:

<style id="foo-bar" pw-file="main.css">
  .foo { color: green }
  .bar { color: blue }
</style>

Following the above, ProcessWire will now be maintaining regions named hello-world and foo-bar in a main.css file. The main.css file (or whatever filename you choose) is located in /site/assets/markup-regions/. You can add as many file regions as you want, whether they all point to the same file or point to multiple files.

How to use file regions for JS

Exactly the same as using it for CSS, but use <script> tags instead. Place the script tag in your head, before the closing body tag, or wherever you want it:

<script src="main.js"></script> 

Then you can populate that main.js file anywhere in your output, from any number of script tags that point to main.js:

<script id="test-js" pw-file="main.js">
  alert('This is a test');
</script>

How to use file regions for SCSS/LESS

This would be the same as for CSS except that you'll be responsible for compiling the SCSS/LESS file, and pointing to the resulting CSS file, just as you would without file regions. Your tag will be left as-is.

<link rel="stylesheet" href="<?=$config->urls->markupRegions?>test.css">

Now you can define your SCSS/LESS anywhere in the output, and it will populate a /site/assets/markup-regions/test.scss (or .less) file. But you'll have to use whatever tool you want to compile it to a CSS file.

<style id="my-test-scss" pw-file="test.scss">
  $alert-background: red;
  $alert-text: white;
  .alert {
     color: $alert-text;
     background-color: $alert-background;
  }
</style>

Let's say that we were using ProCache to compile our SCSS or LESS, we could compile and link to it like this:

$css = $procache->css([ $config->urls->markupRegions . 'test.scss' ]);
echo "<link rel='stylesheet' href='$css'>";

Deleting regions

ProcessWire manages adding and updating regions, but if you need to delete a region, just delete the entire file from /site/assets/markup-regions/ and ProcessWire will re-create it on the next page load, without your deleted region. Perhaps a future version will be able to detect deleted regions somehow or another. But since the same CSS file (for example) might be populated with multiple different regions over multiple requests for different pages, ProcessWire can't assume that it knows the full scope of regions in one file from any single request. For that reason, it can't safely assume that a particular region has been deleted. But it can easily re-create any files that you delete, and doing so will ensure they do not contain CSS or JS for old/deleted regions.

Other tips

For CSS or JS you can specify pw-file as a boolean attribute and it will assume the file "main.css" or "main.js", i.e. <style id="test" pw-file>…</style>

File regions are not meant to replace the traditional way of managing CSS and JS. Instead, see it as another tool to use when it fits the need.

Defining file regions in this way means that you can use PHP code and variables as part of your CSS/JS/SCSS/LESS, should that be useful.

When building components for a site or application, and depending on the case, it can sometimes be helpful to maintain the PHP, markup, CSS and JS together, like in the example below. Just one file to launch something new, and ProcessWire takes care of moving them to externally linked assets.

<ul id="items">
  <?php
  foreach($pages->get('/items/')->children as $item) {
    echo "<li class='item'>$item->title</li>";
  }
  ?>
</ul>

<style id="items-css" pw-file>
  #items .item {
    border-bottom: 1px solid black;
    font-size: 1rem;
  }
</style>

<script id="items-js" pw-file>
  $(function() {
     $('.item').on('click', function() {
       alert("You clicked on: " + $(this).text());
     });
  });
</script>

Click any linked item for full usage details and examples. Hookable methods are indicated with the icon. In addition to those shown below, the WireMarkupFileRegions class also inherits all the methods and properties of: Wire.

Show class?     Show args?       Only hookable?    

Additional methods and properties

In addition to the methods and properties above, WireMarkupFileRegions also inherits the methods and properties of these classes:

API reference based on ProcessWire core version 3.0.266