
Quick start
Step-by-step setup of the wishlist on a MiniShop3 site, similar to MyFavorites: snippets for the button, counter, IDs, and list output without PHP in the template.
Snippet names: ms3FavoritesBtn, ms3FavoritesCounter, ms3FavoritesIds, ms3FavoritesLists.
Fenom examples below assume pdoTools 3.x.
Installation
Requirements
| Requirement | Version |
|---|---|
| MODX Revolution | 3.0+ |
| PHP | 8.1+ |
| MiniShop3 | installed |
| pdoTools | 3.0.0+ |
Via ModStore
- Connect ModStore repository
- Go to Extras → Installer and click Download Extras
- Ensure MiniShop3 and pdoTools are installed
- Find ms3Favorites, click Download, then Install
- Settings → Clear cache
Package is available at modstore.pro.
After installation
Load lexicon, CSS and JS on the site, add the button on the product card and output the wishlist block. Details below.
Step 1: Lexicon, styles and script
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.
Step 2: Add to Wishlist button
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).
Step 3: Wishlist counter
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'].
The value is set on load (1–99 or 99+); the element is hidden when zero.
Step 4: Wishlist block
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}List of named lists (tabs / menu)
Like MyFavorites.lists: 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.
Custom paginated favorites (pdoPage + msProducts)
Use this on a separate resource if you need server-side pagination — not the default /wishlist/ output from ms3FavoritesPage (that page uses JS render() only). 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}If ms3favorites_clear_list is missing in lexicon, use plain text or add the key.
Catalog: pdoPage + msProducts, counter and row button
Not the favorites page: a normal catalog with pagination, favorites button per row (list default), counter on top. 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>Step 5: /wishlist/ page
Create a resource with alias wishlist, a template that loads ms3fLexiconScript, favorites.css and favorites.js. In the content:
[[!ms3FavoritesPage]]{'!ms3FavoritesPage' | snippet}Pagination: ms3FavoritesPage has no embedded pdoPage — cards on /wishlist/ are always filled by favorites.js. For a separate paginated favorites view, use ms3FavoritesIds → pdoPage → ms3Favorites (or msProducts) — see Integration.
Extended toolbar (Catalog / Clear list / Share): pass extendedToolbar or use chunk alias tplFavoritesPageDemo — see ms3FavoritesPage.
Styling (short)
Override CSS variables: --ms3f-button-active, --ms3f-bg, --ms3f-border, --ms3f-color, etc.
:root {
--ms3f-button-active: #e74c3c;
--ms3f-bg: #fff;
--ms3f-color: #333;
}Guest DB cleanup (cron)
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.
Next steps
- System settings — limit, storage type, guests in DB
- Snippets — parameters for ms3Favorites, ms3FavoritesBtn, ms3FavoritesIds, etc.
- Frontend setup — custom chunks and styles
- Integration and customization — advanced scenarios, msProducts, cart integration
