Skip to content

Install and call ​

This page is for templaters: no PHP needed. Everything below is written in a MODX template and in chunks.

Installation ​

  1. Install PageBlocks alfa24 or newer — without it the component switches off and logs why.
  2. Install the pbFavorites package through the Installer.
  3. Check the install log: the migration creates the pb_favorites table and says so in a line of its own.

Migrations run as a separate step

If exec() is disabled on the host, the resolver says so in the log and prints the command. Create the table by hand then:

bash
php core/components/pageblocks/vendor/bin/phinx migrate \
    --configuration=core/components/pbfavorites/src/phinx.php --environment=production

What else has to be installed ​

On its own the package shows nothing, and that is not a defect: it knows no models until somebody registers one. Satellites register them:

  • pbShop alfa44 and newer registers pbshop.product and needs nothing else;
  • your own model is registered by the site in one line — see the developer section.

If favourites "don't work", look at what the button prints first: given an unknown model it answers with text listing the keys it does know, rather than staying silent.

A button on a card ​

[[!pbFavoriteButton? &model=`pbshop.product` &id=`[[+id]]`]]

&model and &id are required. The button knows its own state ("save" or "saved") and changes its label after a click by itself.

Uncached calls only

[[! with the exclamation mark is mandatory in all three snippets. Bookmark state is personal; a cached tag would hand the first visitor's list to everybody else.

With the "how many people saved this" counter:

[[!pbFavoriteButton? &model=`pbshop.product` &id=`[[+id]]` &watchers=`1`]]

Without &watchers the counter is not computed at all — not every storefront wants an extra query per card.

A counter in the header ​

[[!pbFavoritesCount]]

Without &tpl it returns a bare number, no markup: a counter usually goes inside an element of your own, where a wrapper would be in the way.

html
<a class="header-wish" href="/favorites">
    Favourites <span>[[!pbFavoritesCount]]</span>
</a>

Visible bookmarks are counted: whatever is hidden is not printed in the list, and a number above it would promise what the visitor will not see.

Products only — [[!pbFavoritesCount? &model=pbshop.product]].

The list page ​

Create a resource (say, with the favorites alias) and call:

[[!pbFavorites]]

Without &model all of the visitor's bookmarks are printed: on a site with a blog and a shop there is one list. Products only:

[[!pbFavorites? &model=`pbshop.product` &tpl=`myFavoriteRow`]]

The page belongs to the site, not the component

The list deliberately has no URL of its own. On such a site people bookmark articles too, so an address like "/catalogue/favorites" would promise it is a shop section. The resource and its address are yours.

Parameters ​

The vocabulary is shared by all three snippets and matches pbShop.

ParameterWhat it does
&modelmodel key (pbshop.product). Without it — all bookmarks
&idobject id. pbFavoriteButton only, required
&tplrow chunk. For pbFavoritesCount its absence means "return a number"
&tplWrapperlist wrapper chunk; {$output} and {$total} are available inside
&tplEmptychunk for an empty list
&limit, &offsetslice of the list. &limit=0 — no limit
&sortby, &sortdirorder by a row field; newest-first by default
&outputSeparatorwhat glues the rows
&toPlaceholderoutput into a placeholder, the snippet returns nothing
&showLogshow the call log (visible to manager users only)
&watcherscount "how many people saved this". pbFavoriteButton only
&currencyrow currency. Passed through to the provider

Typos do not stay silent

An unknown &model, a missing &id, a non-existent field in &sortby — all of it is printed as text at the call site and goes into &showLog. An empty list and a typo look identical on a storefront, so the component tells them apart for you.

The same four exist in code

&limit, &offset, &sortby and &sortdir are computed by the service, not by the snippet — so the developer next to you gets them from the same call, $favorites->all($model, [], ['limit' => 20]). That matters on a mixed project: a capability that exists only in a template has to be written again sooner or later.

A ready-made "Favourites" section ​

Copy the whole thing. Three snippets, your own row chunk, the script — after that you only touch markup.

1. A resource with the favorites alias and this content:

[[!pbFavorites? &tpl=`myFavoriteRow`]]

2. The myFavoriteRow chunk — start from the shipped pbFavorites.row.tpl. ⚠️ Component chunks are rendered by Fenom, so fields are written {$item.title}, not [[+title]]:

html
<div class="card" data-pbfav-row data-model="{$item.model}" data-id="{$item.item_id}">
    <a href="{$item.url}"><img src="{$item.image}" alt="{$item.title}"></a>
    <a class="card-title" href="{$item.url}">{$item.title}</a>
    {if $item.formatted}<span class="card-price">{$item.formatted}</span>{/if}
    <button type="button" class="card-remove"
            data-pbfav-toggle="{$item.model}" data-pbfav-id="{$item.item_id}" aria-pressed="true">
        {'pbfav_remove'|lexicon}
    </button>
</div>

3. A header link — in the site-wide template:

html
<a href="/favorites">Favourites <span>[[!pbFavoritesCount]]</span></a>

4. A button on a catalogue card — wherever a product is rendered:

[[!pbFavoriteButton? &model=`pbshop.product` &id=`[[+id]]`]]

5. The script and the token — once per template, see "Plug in the script" below.

The row comes from the provider

{$item.formatted} and {$item.image} come from the pbShop product provider. Your own model will have different fields, and whoever wrote the provider knows them.

Your own markup ​

The chain is: &tpl → a chunk in the manager tree → a file from the package.

No chunk records are created at install time. To override the markup, create a chunk with the right name — a package upgrade will not touch it, because it does not touch records at all:

Chunk and fileWhat it renders
pbFavorites.rowa list row
pbFavorites.listthe list wrapper
pbFavorites.emptyan empty list
pbFavorites.buttonthe button
pbFavorites.countthe counter (when called with &tpl)

These chunks are not in the manager tree

That is a deliberate price, not an oversight: the package installs without an element resolver, so no records are created — and the shipped markup cannot be found by browsing the tree. Take the names from the table above.

The files live in core/components/pbfavorites/elements/chunks/ and are named exactly like the records: pbFavorites.row.tpl, pbFavorites.list.tpl and so on. Shortest path: copy the file, create a chunk with the same name, edit that.

Chunks are rendered by Fenom, not by the MODX parser: inside you write {$item.title}, not [[+title]]. The full file template name is file:pbFavorites:pbFavorites.row, but there is usually no reason to spell it in &tpl — with no parameter the default finds itself.

What a row contains ​

A row is assembled by the provider, so the set of fields depends on the model. The package adds two of its own:

FieldSource
{$item.model}package: the model key
{$item.item_id}package: the object id
{$item.title}, {$item.url}, {$item.image}, {$item.formatted}pbShop's product provider

Read fields softly — whatever the provider did not give is simply not printed:

{if $item.formatted}<span class="price">{$item.formatted}</span>{/if}

What the script needs ​

The package's script (assets/components/pbfavorites/js/favorites.js) looks for attributes in the markup. If you write your own chunks, keep them:

AttributeOn whatWhy
data-pbfav-toggle="<model>" + data-pbfav-idthe buttontoggles on click
data-pbfav-label-add, data-pbfav-label-inthe buttonstate labels; without them the script leaves the text alone
data-pbfav-countany elementgets the number
data-pbfav-rowa list rowa removed row disappears
data-pbfav-cleara buttonclears the list, in two clicks
data-pbfav-confirmthe same buttonthe confirmation label
data-pbfav-errorany elementwhere a server refusal is printed

The script does not assemble markup

New rows do not appear without a reload, and that is a decision: if the script built them, the markup would live in two places — the chunk and the JS — and an edit to the chunk would quietly fail to reach the re-rendered list. People bookmark in the catalogue and review the list later; the two pages are rarely open at once.

Hooking up the script ​

The package does not inject itself into your page: somebody else's template is not its business. Add this to your template:

html
<script src="/assets/components/pbfavorites/js/favorites.js" defer></script>

And the token — the package posts its requests and PageBlocks verifies them. In a Fenom file template that is one line in <head>:

{meta_csrf}

The script looks for the token in meta[name="csrf-token"] and, failing that, in an input[name="_token"] on the page. So in a MODX template, where Fenom functions are unavailable, it is enough that the page carries any PageBlocks form with its hidden token field.

Without a token the button "just doesn't work"

The request goes out and comes back refused, which on a storefront looks like a broken button. If a click does nothing, check the /favorites/toggle response in the console first.

Without the script ​

The button needs JS — it sends a request. If you do not want the script, the list still prints: a page with [[!pbFavorites]] shows what was saved without a single line of JavaScript.

pbFavorites — bookmarks on any model, free for PageBlocks