Skip to content
  1. Extras
  2. ms3Favorites
  3. Quick start

Quick start ​

Step-by-step integration of the wishlist with a MiniShop3 site.

Snippet names: ms3FavoritesBtn, ms3FavoritesCounter, ms3FavoritesIds, ms3FavoritesLists.

Fenom examples below assume pdoTools 3.x.

Installation ​

Requirements ​

RequirementVersion
MODX Revolution3.0+
PHP8.1+
MiniShop3installed
pdoTools3.0.0+

Via ModStore ​

  1. Connect ModStore repository
  2. Go to Extras → Installer and click Download Extras
  3. Ensure MiniShop3 and pdoTools are installed
  4. Find ms3Favorites, click Download, then Install
  5. 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.

fenom
{'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>
modx
[[!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:

modx
[[!ms3FavoritesBtn? &id=`[[*id]]`]]
fenom
{'!ms3FavoritesBtn' | snippet : ['id' => $_modx->resource.id]}

In the product card chunk (e.g. inside msProducts output):

modx
[[!ms3FavoritesBtn? &id=`[[+id]]`]]
fenom
{'!ms3FavoritesBtn' | snippet : ['id' => $id]}

Reload page after remove (optional):

modx
[[!ms3FavoritesBtn? &id=`[[*id]]` &remove=`1`]]
fenom
{'!ms3FavoritesBtn' | snippet : ['id' => $_modx->resource.id, 'remove' => 1]}

Wishlist-box layout (li.wishlist, box-icon, icon-heart, tooltip) — chunk tplMs3fBtnWishlistBox:

modx
[[!ms3FavoritesBtn? &id=`[[*id]]` &tpl=`tplMs3fBtnWishlistBox`]]
fenom
{'!ms3FavoritesBtn' | snippet : ['id' => $_modx->resource.id, 'tpl' => 'tplMs3fBtnWishlistBox']}

Remove a wrapper by ID prefix (e.g. #product-item-{id}):

modx
<div id="product-item-[[+id]]">
  ...
  [[!ms3FavoritesBtn? &id=`[[+id]]` &remove=`product-item`]]
</div>
fenom
<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):

html
<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:

modx
[[!ms3FavoritesCounter]]
fenom
{'!ms3FavoritesCounter' | snippet}
modx
<a href="/wishlist/">Wishlist [[!ms3FavoritesCounter]]</a>
fenom
<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.

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:

html
<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:

modx
[[!ms3FavoritesIds? &toPlaceholder=`favorites_ids`]]
[[!+favorites_ids:is=`-0`:then=`
  <p>[[%ms3favorites_empty]]</p>
`:else=`
  [[!ms3Favorites?
    &ids=`[[+favorites_ids]]`
    &tpl=`tplFavoritesItem`
    &emptyTpl=`tplFavoritesEmpty`
  ]]
`]]
fenom
{'!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) ​

Snippet ms3FavoritesLists outputs the user’s lists with counts. Links use System setting ms3favorites.list_page (default wishlist/).

modx
[[!ms3FavoritesLists? &tplWrapper=`tplMs3fListsWrapper`]]
fenom
{'!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 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”:

modx
[[!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]]
`]]
fenom
{'!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}

Catalog: pdoPage + msProducts, counter and row button ​

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.

modx
<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>
fenom
<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>

Pagination: pdoPage + ms3Favorites ​

Same ms3FavoritesIds flow, but pdoPage wraps snippet ms3Favorites instead of msProducts. Parameters page, offset, totalVar on ms3Favorites work with pdoPage. Details — Integration.

modx
[[!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]]
`]]
fenom
{'!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}

Step 5: /wishlist/ page ​

Create a resource with alias wishlist, a template that loads ms3fLexiconScript, favorites.css, and favorites.js. In the content:

modx
[[!ms3FavoritesPage]]
fenom
{'!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.

Styling (short) ​

Override CSS variables: --ms3f-button-active, --ms3f-bg, --ms3f-border, --ms3f-color, etc.

css
: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:

bash
php /path/to/site/core/components/ms3favorites/cli/cleanup_guests.php

The CLI script resolves MODX from config.core.php (walks up from cli/). If guest_ttl_days = 0, TTL cleanup is skipped.

Next steps ​