Three primitives, three jobs
When you extend a MAM Suite app’s UI in PHP, you reach for one of three primitives. They are easy to confuse because they all “add something the user sees,” but they live at different layers of the rendering pipeline and answer different questions:
- A content class answers “what kind of screen is this?” It is the per-screen handler — Login, Map, Web URL, WP Post Category (a list), WP Post Content (a detail view). It decides the whole screen’s shape and produces the data payload the mobile app renders.
- A content class element answers “what reusable section goes inside this screen?” It is a block — a featured image, an add-to-cart button, an order-details panel — that a content class assembles into its output. Elements compose within a screen.
- A tab-bar button answers “what action can the user take from this screen?” It is an action affixed to a detail screen’s tab bar — Edit, Share, Call, Open Form — populated per item, per user, at request time.
Getting this distinction right matters because choosing the wrong primitive means fighting the framework. Building a whole new screen type as an “element” leaves you with nowhere to register it; cramming a per-item action into a content class means re-deriving it for every list row by hand. Each primitive has a registration mechanism, a contract, and a place in the pipeline that the others do not.
This article is grounding for developers. The authoritative per-primitive references are Content classes overview, Content class elements reference, and Recipe: Register a tab-bar button.
Content classes: the screen handler
A content class represents one type of mobile screen or button — there are about twenty of them registered in mam-main/includes/content-classes/. Each lives in a single file, registers itself into the global $local_app_content array under its content-type key ($local_app_content['Web_URL'] = array( 'content_type' => …, 'class' => …, 'vc_type' => … )), and implements a duck-typed interface (there is no formal interface declaration yet). The key methods are app_settings_categories() and app_settings() (the per-button settings UI), the form/HTML methods the admin sees, and get_data_for_app($category) — the method that builds the payload the device actually receives.
A content class is the right extension point when you are introducing a genuinely new kind of screen or top-level button that the no-code builder doesn’t already offer: a new viewer, a new list source, a new interactive button type. Adding one gives customers a new building block to assemble screens with.
Two things make content classes the “heaviest” primitive:
- Frozen class names. Class names like
local_app_login_buttonare frozen because they get serialized into every customer’slocal-app-button-array*option. Renaming one orphans every site’s saved instance of that button. Adding a new class is safe; renaming or removing an existing one is a coordinated migration. - Registration is two-step. You create the file under
includes/content-classes/, add it to therequire_oncechain incontent-class-manager.php, and register it in$local_app_content(keyed by content-type) withcontent_type,class, andvc_typekeys. (vc_typeis a Visual Composer leftover that is mostly inert but still read downstream — keep it set.)
If your idea is “a new section that appears inside an existing screen” or “a new button on the detail tab bar,” you do not want a content class. Read on.
Elements: reusable sections inside a screen
A content class element is a reusable UI section that content classes compose into their per-screen output. Rather than every content class duplicating the markup-shape logic for “add to cart” or “featured image” or “order details,” it fires a filter and gets back a content_sections[] entry to append. Elements live in includes/content-class-elements/general-elements.php as a procedural file of filter callbacks — there is no central registry; you discover them by reading the file.
The output of an element is one entry in the screen’s content_sections array:
"content_sections": [
{ "title": "Featured", "content": "...", "type": "featured_image", "show_title": false },
{ "title": "Order details", "content": "...", "type": "order_details", "show_title": true }
]
A content class invokes an element by applying its filter, for example:
$data_array = apply_filters( 'mam_main_content_element_add_to_cart', $data_array, $form_manager );
(Most element filters are named mam_main_content_element_*, but a couple of older ones — the featured-image and reviews blocks — are registered as mam_main_content_class_featured_image and mam_main_content_class_reviews. The prefix is historical, not a second kind of thing; grep both when you go looking.)
Reach for an element when you want a section of content that more than one screen could reuse, or when you want to override how an existing section behaves. Several elements expose an *_args sub-filter (for example mam_main_content_element_add_to_cart_args) so you can change labels or login-prompt copy without touching core. Elements are pure data transformations — no DB reads, no options, no globals — which is what makes them safe to compose freely.
One naming trap worth flagging: the terms are not interchangeable. A content class is the per-screen handler; an element is a section that handler assembles. The codebase even has a stub/live pair (mam_main_content_element_show_shipping_status is a stub returning input unchanged; mam_main_content_element_shipping_status is the live implementation), which sibling plugins replace — see the Content class elements reference for the full list.
Tab-bar buttons: per-item actions on a detail screen
A tab-bar button is an action affixed to a detail screen’s tab bar — Edit, Share, Call, Open Form. Unlike a content class (which defines the screen) and an element (which adds content to it), a tab-bar button adds a thing the user can do, and it is resolved per item and per user at request time.
Tab-bar buttons follow a three-step pattern:
- Register the definition. A content class (or a small registry class) appends an entry via the
mam_app_settings_get_tab_bar_buttonsfilter with anid,title, andtype. This makes the button selectable in the admin UI. - Admin enables it. In the tab-bar admin (Mobile App Manager → Navigation), the admin checks “Show Button” so the setting’s
visibleflag is'on'. The runtime filter does not fire unless this is set. - Populate at runtime. You implement
mam_main_add_tab_bar_item_{slug}to build the button per item, per user — set its icon, action, source, and anyvalues.
The slug you register, the {slug} segment of the filter name, and the id your filter returns must all match exactly, or you get silent “missing tabbar” debug entries and no button.
Reach for a tab-bar button when the thing you’re adding is a per-item action on a detail screen and you need per-user authorization. The runtime filter is the place to gate visibility: return an empty array [] to hide the button (never a partially filled array — mam-main treats any non-empty array as “render this”). Authorization is your responsibility; mam-main dispatches but does not gate by role.
Two practical constraints shape how you write the runtime filter:
- It’s a hot path. The filter runs once per item per app load — a 50-item list means 50 invocations. Cache role lookups; avoid HTTP calls.
- The action token is interpreted by the mobile client. Common actions (
open_form,share_listing,phone_call,web_url) already exist; adding a new action type requires coordination with the app team.
How they compose: a list-with-detail screen
The clearest way to see the three primitives working together is the most common app pattern — a list of things, each opening a detail screen. MAM Suite builds this from two cooperating content classes plus elements and tab-bar buttons layered on the detail screen:
-
The list screen is one content class. In core that is WP Post Category (
local-app-wp-post-category-class.php); in the WooCommerce family it is a product-list class. Itsget_data_for_app()returns the rows the app renders as a scrollable list, each row carrying the identifier the app uses to request a detail screen. -
The detail screen is another content class. In core that is WP Post Content (
local-app-wp-post-content-class.php); in commerce apps it is a product-detail or order-detail class. When the user taps a row, the app requests this screen for that item and the class assembles the detail view. -
The detail screen’s body is built from elements. Inside the detail content class’s
get_data_for_app(), the handler applies element filters to append a featured image, an add-to-cart section, an order-details panel, a reviews block, and so on — each one acontent_sections[]entry. In mam-main core, the plain WP Post Content class mostly just runsthe_content; the rich element composition lives in the WooCommerce-family and link-library detail classes (mam-woocommerce,mam-woocommerce-product-vendors,mam-link-library), which is where filters likemam_main_content_element_add_to_cartandmam_main_content_element_order_detailsare actually applied. Either way the point holds: a new “section on the detail page” is an element, not a content class — the detail screen already exists, and you’re contributing a block to it. -
The detail screen’s actions are tab-bar buttons. Edit, Share, and Call sit on the detail tab bar, each registered as a definition, enabled by the admin, and populated per item by a
mam_main_add_tab_bar_item_{slug}filter that reads that specific item’s data and the current user.
So the decision tree falls out naturally:
- New kind of list, or a new viewer/screen entirely? → content class.
- New section of content inside a screen that already exists? → element.
- New action a user can take from a detail screen, gated per user? → tab-bar button.
Where each one sits in the pipeline
A mobile request flows through mam_get_phone_data() into the phone-data pipeline. For each button in the role’s button array, the pipeline resolves the content_class from $local_app_content and calls get_data_for_app(). That call is where elements get composed in (the content class applies element filters) and where tab-bar buttons get dispatched (per item, via the {slug} filters). In other words:
- Content classes are invoked by the pipeline’s button loop.
- Elements are invoked by a content class, during its own
get_data_for_app(). - Tab-bar buttons are dispatched for a detail-screen content class, once per item.
They nest, they don’t compete. Pick the primitive that matches the layer your change belongs to, and the framework’s registration and contracts will be on your side instead of in your way.
What’s next
- Content classes overview — the 21 classes, the duck-typed interface, the settings schema, and how to add a new class.
- Content class elements reference — the full list of element filters and their
*_argssub-filters. - Recipe: Register a tab-bar button — the end-to-end three-step recipe, including authorization and the
mam_main_skip_tab_bar_buttonescape hatch. - Recipe: Register a content class — the file/registry steps for a brand-new screen type.
