
- MODX 3
- PHP 8.1
- miniShop3


Step-by-step integration of the wishlist with a MiniShop3 site.
Snippet names: ms3FavoritesBtn, ms3FavoritesCounter, ms3FavoritesIds, ms3FavoritesLists.
Fenom examples below assume pdoTools 3.x.
| Requirement | Version |
|---|---|
| MODX Revolution | 3.0+ |
| PHP | 8.1+ |
| MiniShop3 | installed |
| pdoTools | 3.0.0+ |
Package is available at modstore.pro.
Load lexicon, CSS and JS on the site, add the button on the product card and output the wishlist block. Details below.
In the template (or shared head/footer), load first the lexicon, then CSS and JS.
{'ms3fLexiconScript' | snippet}
<link rel="stylesheet" href="{'assets_url' | config}components/ms3favorites/css/favorites.css">
<script src="{'assets_url' | config}components/ms3favorites/js/favorites.js"></script>[[!ms3fLexiconScript]]
<link rel="stylesheet" href="[[++assets_url]]components/ms3favorites/css/favorites.css">
<script src="[[++assets_url]]components/ms3favorites/js/favorites.js"></script>Fenom and auto_escape
If the page goes blank after adding these lines, check the MODX log for ms3fLexiconScript errors. With Fenom auto_escape enabled, output the snippet as raw HTML, e.g. {raw ('ms3fLexiconScript' | snippet)} (exact syntax depends on your Fenom version).
Without ms3fLexiconScript, lexicon keys may show instead of translated strings; the JS still works with built-in Russian fallbacks.
Add the button on the product card — snippet ms3FavoritesBtn:
[[!ms3FavoritesBtn? &id=`[[*id]]`]]{'!ms3FavoritesBtn' | snippet : ['id' => $_modx->resource.id]}In the product card chunk (e.g. inside msProducts output):
[[!ms3FavoritesBtn? &id=`[[+id]]`]]{'!ms3FavoritesBtn' | snippet : ['id' => $id]}Reload page after remove (optional):
[[!ms3FavoritesBtn? &id=`[[*id]]` &remove=`1`]]{'!ms3FavoritesBtn' | snippet : ['id' => $_modx->resource.id, 'remove' => 1]}Wishlist-box layout (li.wishlist, box-icon, icon-heart, tooltip) — chunk tplMs3fBtnWishlistBox:
[[!ms3FavoritesBtn? &id=`[[*id]]` &tpl=`tplMs3fBtnWishlistBox`]]{'!ms3FavoritesBtn' | snippet : ['id' => $_modx->resource.id, 'tpl' => 'tplMs3fBtnWishlistBox']}Remove a wrapper by ID prefix (e.g. #product-item-{id}):
<div id="product-item-[[+id]]">
...
[[!ms3FavoritesBtn? &id=`[[+id]]` &remove=`product-item`]]
</div><div id="product-item-{$id}">
...
{'!ms3FavoritesBtn' | snippet : ['id' => $id, 'remove' => 'product-item']}
</div>Snippet parameters: list, tpl, remove, classes, resource_type, label — see ms3FavoritesBtn.
On click the product is added or removed from the list. A notification is shown automatically (iziToast / MiniShop3 ms3Message / your own ms3fConfig.notify — see Integration).
Client-side (JS fills the value on load):
<a href="/wishlist/">
<span>Wishlist</span>
<span data-favorites-count style="display: none;">0</span>
</a>Limit the counter to one resource_type (e.g. only products): <span data-favorites-count data-resource-type="products"></span>.
Server snippet ms3FavoritesCounter:
[[!ms3FavoritesCounter]]{'!ms3FavoritesCounter' | snippet}<a href="/wishlist/">Wishlist [[!ms3FavoritesCounter]]</a><a href="/wishlist/">Wishlist {'!ms3FavoritesCounter' | snippet}</a>Total across all lists: &list=all / ['list' => 'all'].
After load the script sets the counter to a number from 1 to 99, or to 99+ when there are more items. When the total is zero the element stays hidden.
Client-side render (JS):
Use class ms3f__list for flex layout and horizontal scroll on small screens. The container is filled from localStorage/cookie via the connector:
<div id="wishlist-list" class="ms3f__list"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (window.ms3Favorites) {
window.ms3Favorites.render('#wishlist-list');
}
});
</script>Optional render options: limit, tpl, emptyTpl, list, resource_type — same names as for the connector.
Server output:
Get favorite IDs into a placeholder — snippet ms3FavoritesIds:
[[!ms3FavoritesIds? &toPlaceholder=`favorites_ids`]]
[[!+favorites_ids:is=`-0`:then=`
<p>[[%ms3favorites_empty]]</p>
`:else=`
[[!ms3Favorites?
&ids=`[[+favorites_ids]]`
&tpl=`tplFavoritesItem`
&emptyTpl=`tplFavoritesEmpty`
]]
`]]{'!ms3FavoritesIds' | snippet : ['toPlaceholder' => 'favorites_ids']}
{set $idsStr = $_modx->getPlaceholder('favorites_ids')}
{if $idsStr == '-0'}
<p>{$_modx->lexicon('ms3favorites_empty')}</p>
{else}
{'!ms3Favorites' | snippet : [
'ids' => $idsStr,
'tpl' => 'tplFavoritesItem',
'emptyTpl' => 'tplFavoritesEmpty'
]}
{/if}Snippet ms3FavoritesLists outputs the user’s lists with counts. Links use System setting ms3favorites.list_page (default wishlist/).
[[!ms3FavoritesLists? &tplWrapper=`tplMs3fListsWrapper`]]{'!ms3FavoritesLists' | snippet : ['tplWrapper' => 'tplMs3fListsWrapper']}Without tplWrapper you get only rows from tplMs3fListsRow.
Use this on a separate resource if you need a custom paginated layout. On /wishlist/, ms3FavoritesPage already uses pdoPage + msProducts when serverList=1 (default) and resource_type=products. Flow: IDs → empty check → paged output and “Clear list”:
[[!ms3FavoritesIds? &toPlaceholder=`myf.ids`]]
[[!+myf.ids:is=`-0`:then=`
<p>[[%ms3favorites_empty]]</p>
`:else=`
[[!pdoPage?
&element=`msProducts`
&parents=`0`
&limit=`12`
&resources=`[[!+myf.ids]]`
&sortby=`FIELD(msProduct.id, [[!+myf.ids]])`
]]
<button type="button" class="btn btn-primary" data-favorites-clear>[[%ms3favorites_clear_list]]</button>
[[!+page.nav]]
`]]{'!ms3FavoritesIds' | snippet : ['toPlaceholder' => 'myf.ids']}
{set $idsStr = $_modx->getPlaceholder('myf.ids')}
{if $idsStr == '-0'}
<p>{$_modx->lexicon('ms3favorites_empty')}</p>
{else}
{'pdoPage' | snippet : [
'element' => 'msProducts',
'parents' => 0,
'limit' => 12,
'resources' => $idsStr,
'sortby' => 'FIELD(msProduct.id, ' ~ $idsStr ~ ')'
]}
<button type="button" class="btn btn-primary" data-favorites-clear>{$_modx->lexicon('ms3favorites_clear_list')}</button>
{$_modx->getPlaceholder('page.nav')}
{/if}Not the favorites page: a normal catalog with pagination. Each row uses list default and includes the favorites control; the counter sits above the listing. Use package chunk tplCatalogRowMs3f. Full notes and AJAX — Integration.
<p>Favorites [[!ms3FavoritesCounter? &list=`default` &resource_type=`products`]]</p>
[[!pdoPage?
&element=`msProducts`
&parents=`0`
&limit=`10`
&tpl=`tplCatalogRowMs3f`
&totalVar=`page.total`
&pageNavVar=`page.nav`
]]
<nav class="pagination">[[!+page.nav]]</nav><p>Favorites {'!ms3FavoritesCounter' | snippet : ['list' => 'default', 'resource_type' => 'products']}</p>
{'!pdoPage' | snippet : [
'element' => 'msProducts',
'parents' => 0,
'limit' => 10,
'tpl' => 'tplCatalogRowMs3f',
'totalVar' => 'page.total',
'pageNavVar' => 'page.nav'
]}
<nav class="pagination">{$_modx->getPlaceholder('page.nav')}</nav>Same ms3FavoritesIds flow, but pdoPage wraps snippet ms3Favorites instead of msProducts. Parameters page, offset, totalVar on ms3Favorites work with pdoPage. Details — Integration.
[[!ms3FavoritesIds? &toPlaceholder=`myf.ids` &list=`default`]]
[[!+myf.ids:is=`-0`:then=`
<p>[[%ms3favorites_empty]]</p>
`:else=`
[[!pdoPage?
&element=`ms3Favorites`
&ids=`[[!+myf.ids]]`
&list=`default`
&limit=`12`
&tpl=`tplFavoritesItem`
&emptyTpl=`tplFavoritesEmpty`
&totalVar=`page.total`
&pageNavVar=`page.nav`
]]
[[!+page.nav]]
`]]{'!ms3FavoritesIds' | snippet : ['toPlaceholder' => 'myf.ids', 'list' => 'default']}
{set $idsStr = $_modx->getPlaceholder('myf.ids')}
{if $idsStr == '-0'}
<p>{'ms3favorites_empty' | lexicon}</p>
{else}
{'!pdoPage' | snippet : [
'element' => 'ms3Favorites',
'ids' => $idsStr,
'list' => 'default',
'limit' => 12,
'tpl' => 'tplFavoritesItem',
'emptyTpl' => 'tplFavoritesEmpty',
'totalVar' => 'page.total',
'pageNavVar' => 'page.nav'
]}
{$_modx->getPlaceholder('page.nav')}
{/if}Create a resource with alias wishlist, a template that loads ms3fLexiconScript, favorites.css, and favorites.js. In the content:
[[!ms3FavoritesPage]]{'!ms3FavoritesPage' | snippet}Default behavior: with resource_type=products and serverList=1 (default), product cards on /wishlist/ are rendered server-side in the chunk (pdoPage + msProducts). With serverList=0, favorites.js fills the list (up to 100 items per tab). For other resource types the list is always JS after sync. See Integration and ms3FavoritesPage.
For a separate paginated favorites page (custom template), use ms3FavoritesIds → pdoPage → ms3Favorites (or msProducts) — examples above and in Integration.
Extended toolbar (Catalog / Clear list / Share): pass extendedToolbar or use chunk alias tplFavoritesPageDemo — see ms3FavoritesPage.
Override CSS variables: --ms3f-button-active, --ms3f-bg, --ms3f-border, --ms3f-color, etc.
:root {
--ms3f-button-active: #e74c3c;
--ms3f-bg: #fff;
--ms3f-color: #333;
}To purge expired guest rows by guest_ttl_days, schedule:
php /path/to/site/core/components/ms3favorites/cli/cleanup_guests.phpThe CLI script resolves MODX from config.core.php (walks up from cli/). If guest_ttl_days = 0, TTL cleanup is skipped.