Install and call
This page is for templaters: no PHP needed. Everything below is written in a MODX template and in chunks.
Installation
- Install PageBlocks alfa24 or newer — without it the component switches off and logs why.
- Install the pbFavorites package through the Installer.
- Check the install log: the migration creates the
pb_favoritestable 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:
php core/components/pageblocks/vendor/bin/phinx migrate \
--configuration=core/components/pbfavorites/src/phinx.php --environment=productionWhat 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
alfa44and newer registerspbshop.productand 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.
<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.
| Parameter | What it does |
|---|---|
&model | model key (pbshop.product). Without it — all bookmarks |
&id | object id. pbFavoriteButton only, required |
&tpl | row chunk. For pbFavoritesCount its absence means "return a number" |
&tplWrapper | list wrapper chunk; {$output} and {$total} are available inside |
&tplEmpty | chunk for an empty list |
&limit, &offset | slice of the list. &limit=0 — no limit |
&sortby, &sortdir | order by a row field; newest-first by default |
&outputSeparator | what glues the rows |
&toPlaceholder | output into a placeholder, the snippet returns nothing |
&showLog | show the call log (visible to manager users only) |
&watchers | count "how many people saved this". pbFavoriteButton only |
¤cy | row 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]]:
<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:
<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 file | What it renders |
|---|---|
pbFavorites.row | a list row |
pbFavorites.list | the list wrapper |
pbFavorites.empty | an empty list |
pbFavorites.button | the button |
pbFavorites.count | the 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:
| Field | Source |
|---|---|
{$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:
| Attribute | On what | Why |
|---|---|---|
data-pbfav-toggle="<model>" + data-pbfav-id | the button | toggles on click |
data-pbfav-label-add, data-pbfav-label-in | the button | state labels; without them the script leaves the text alone |
data-pbfav-count | any element | gets the number |
data-pbfav-row | a list row | a removed row disappears |
data-pbfav-clear | a button | clears the list, in two clicks |
data-pbfav-confirm | the same button | the confirmation label |
data-pbfav-error | any element | where 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:
<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.