@smartsoft001-mobilems/angular
v2.100.0
Published
Shared Angular building blocks — components, services, directives, guards, state and models.
Downloads
1,338
Readme
@smartsoft001-mobilems/angular
Shared Angular building blocks — components, services, directives, guards, state and models.
Related packages: models · objects · articles · public-collections
Install / import
import { MasonryGridComponent, FiltersBaseComponent, FileUrlService } from '@smartsoft001-mobilems/angular';Root wiring lives in app.config.ts via SharedModule.forRoot(config); feature libraries use
SharedModule (or SharedModule.forFeature(config)) in their module imports.
Contents
- Components
- Base components (extend these)
- Services
- State
- Directives
- Pipes
- Guards
- Models, interfaces and tokens
- Module & config
- Bootstrap entry points
- SSR server (
ssr) - Translations
Components
Ready-to-use components. Selectors are prefixed smart-mobilems-.
| Component | Selector | Purpose |
| ----------------------- | ------------------------------ | -------------------------------------------------------------------------- |
| MasonryGridComponent | smart-mobilems-masonry-grid | Masonry layout with image-load awaiting, responsive gutter and re-layout |
| AppComponent | smart-mobilems-app | App shell root (extends the framework AppBaseComponent) |
| HeaderComponent | smart-mobilems-header | Header state: WCAG toggle, search panel, user, local-collection counter |
| FooterComponent | smart-mobilems-footer | Footer state driven by config + CMS static pages |
| MenuComponent | smart-mobilems-menu | Config-driven main menu |
| PageComponent | — | CMS page renderer (extends PageBaseComponent) |
| ScrollTopComponent | — | Back-to-top button |
| WidgetComponent | smart-widget | Renders an ArticleWidget by mapping ArticleWidgetType → component |
| WidgetTextComponent | smart-widget-text | Text widget |
| PageSectionsComponent | smart-mobilems-page-sections | Dynamic CMS page sections: type/theme → component, portal_slot slot |
| SectionLinkComponent | smart-mobilems-section-link | Wraps content in a router link / external link / <span> for a CMS target |
| GridContainerSectionComponent | smart-mobilems-page-section-grid-container | Default grid_container section: title + children on a 12-column grid |
| ModalContainerComponent | smart-mobilems-modal-container | Host for ModalService.open() |
Pages shipped for reuse: HomeComponent, PageComponent (pages/page), NotFoundComponent.
PageSectionsComponent
Renders the sections of a CMS page in position order. Like WidgetComponent, it maps each
section to a component — { ...baseMap, ...PAGE_SECTION_COMPONENTS_TOKEN }, where the framework
baseMap is empty: visual variants are per museum. Keys are `${type}/${theme}` (one
variant) or bare type (every theme of that type); type/theme wins
(pageSectionComponent(section, map) does the lookup). Section components extend
PageSectionBaseComponent (section input).
@Component({
selector: 'app-home',
imports: [PageSectionsComponent],
providers: [
{
provide: PAGE_SECTION_COMPONENTS_TOKEN,
useValue: {
'slider/carousel': CarouselSectionComponent,
tile_collection: TilesSectionComponent,
},
},
],
template: `
<smart-mobilems-page-sections
[page]="'slug/strona-glowna'"
(pageLoaded)="setSeo($event)"
>
<app-object-of-the-day />
</smart-mobilems-page-sections>
`,
})
export class HomePage {
setSeo(page: Page | null): void {
// page?.seo?.title, page?.seo?.description …
}
}
@Component({
selector: 'app-carousel-section',
template: `<h2 [innerHTML]="section().content?.title"></h2>`,
})
export class CarouselSectionComponent extends PageSectionBaseComponent {}| Input / output | Type | Notes |
| -------------- | ------------------------------------ | ------------------------------------------------------------------------ |
| page | PageRef \| null | Fetched via ConfigsFacade.getPage; re-fetched on change, stale responses ignored |
| sections | PageSectionRaw[] \| null \| undefined | When not undefined it is rendered (after normalization) and no fetch happens (SSR/resolver) |
| slotFallback | boolean (default true) | Without a portal_slot the projected content goes after the sections; false → only at a placeholder |
| expandDictionaries | boolean (default true) | Expand whole-dictionary blocks into one block per entry (see Dictionary targets); false → passed on as they are (not clickable) |
| pageLoaded | output Page \| null | After every fetch; null on API error (no sections rendered, nothing thrown) |
loadedPage is a readonly signal with the last fetched page. The fetch is registered with
PendingTasks, so SSR waits for the sections.
Normalization. Before dispatch the sections go through normalizePageSections(raw) (pure,
exported): null/missing blocks, cta, children (of a grid_container), widget.search,
widget.counter become [], null items are dropped, and sections, blocks, CTAs, children and
widgets are sorted stably by position (ties keep the API order; a missing/null position goes
last and is replaced by the item's index in the sorted list, so position is always a number). content, appearance and widget may still be null — variants must cope. A variant
always receives a normalized PageSection; the API shape is PageSectionRaw.
Each section is wrapped in <section data-testid="page-section"> with an id (section-<position>
when the position is unique among the rendered sections, section-<position>-<index> for ties —
index = place in the normalized list — or the anchor of the previous section's #anchor CTA), data-section-type/data-section-theme,
data-section-bg="color|image|none", the hook class PAGE_SECTION_HOOK_CLASS
(smart-mobilems-page-section) and the appearance as custom properties
(pageSectionAppearance(appearance, multimediaUrl) → PageSectionAppearanceStyle):
| Variable | From | Value |
| -------------------------------------------- | ---------------------------------------- | -------------------------------------------------- |
| --page-section-bg | background.color | the colour |
| --page-section-bg-image | background.file (not type: 'video') | url("<multimediaUrl + filePath>") (pageSectionFileUrl) |
| --page-section-mt / -mr / -mb / -ml | margin.top/right/bottom/left | calc(var(--page-section-space-unit, 0.5rem) * N) |
| --page-section-pt / -pr / -pb / -pl | padding.top/right/bottom/left | same |
A side that is null sets no variable, so the museum fallback applies; N is clamped to
0–PAGE_SECTION_SPACE_MAX (12, the CMS scale). The framework paints nothing — the museum applies the
variables in its own CSS (Tailwind does not scan node_modules), plus its dark / high-contrast
override:
.smart-mobilems-page-section {
--page-section-space-unit: 0.5rem;
margin: var(--page-section-mt, 0) var(--page-section-mr, 0) var(--page-section-mb, 0)
var(--page-section-ml, 0);
padding: var(--page-section-pt, 2rem) var(--page-section-pr, 1rem) var(--page-section-pb, 2rem)
var(--page-section-pl, 1rem);
background-color: var(--page-section-bg, transparent);
}
.smart-mobilems-page-section[data-section-bg='image'] {
background-image: var(--page-section-bg-image);
background-size: cover;
background-position: center;
}Sections without a component are skipped with a console.warn in dev mode only.
grid_container. The only type with a framework default: GridContainerSectionComponent
(smart-mobilems-page-section-grid-container, key PAGE_SECTION_GRID_CONTAINER_TYPE; register
grid_container or grid_container/<theme> to replace it). Inside the container <section> it
renders the optional content.title as <h2 class="smart-mobilems-page-section__title"> and the
children through the same type/theme dispatch, each in a
<section class="smart-mobilems-page-section smart-mobilems-page-grid__item" data-testid="page-section-child">
with its own appearance variables. From 768px .smart-mobilems-page-grid is a 12-column CSS grid
(repeat(12, minmax(0, 1fr))) and each child sits at gridArea(child.grid) (a child without grid
spans the full row); below that the children stack in position order. The layout CSS is added once
per document as <style id="smart-mobilems-page-grid-style"> in <head> (SSR too, CSP_NONCE
honoured) — the museum may override it. Gap:
--page-section-grid-gap (default 0). A nested grid_container (the CMS does not allow one) or a
child without a component is skipped with a dev-mode warning.
Slot (portal_slot). The first section of type PAGE_SECTION_PLACEHOLDER_TYPE ('portal_slot'
— the CMS v3 name: theme default, content: null, no blocks/cta; matched by type whatever the
theme) renders the content projected into the component at its position, inside a fixed
<div class="smart-mobilems-page-slot" data-testid="page-slot"> (PAGE_SECTION_SLOT_CLASS; no
<section> — the host content brings its own semantics). When the portal_slot sets a background or
spacing, the wrapper also gets PAGE_SECTION_HOOK_CLASS (smart-mobilems-page-section), its
appearance variables and data-section-bg like a section. Otherwise (no appearance, while loading,
without a placeholder) it is display: contents without the hook class: it paints nothing and the
projected elements stay flex/grid items of the host (e.g. smart-flex-1 filling a
smart-flex smart-flex-col host keeps working). This is how a CMS page wraps an existing module
(e.g. the object list): sections above and below it, the module in place of portal_slot.
Further placeholders are ignored.
Without a portal_slot — also while the page is loading, after a failed fetch (404, network) and for
a page with empty/missing sections — the projected content falls back to after all sections
(alone on error) when slotFallback is true (default). With slotFallback = false it renders only
at a portal_slot (not while loading; Angular still instantiates it, so keep it light).
The slot wrapper is one fixed container between two loops — sections before the portal_slot, then the
slot, then sections after it (without a placeholder every section goes "before"). Loading the
sections only inserts siblings around it, so the projected module renders immediately (no waiting
for the CMS, no SSR flicker) and is never moved or re-created — it keeps its state (only its class
and style change when the appearance arrives).
Target links. smart-mobilems-section-link (target, label, linkClass, testId) renders a
routerLink, an external <a> (web links with target="_blank" rel="noopener noreferrer",
mailto:/tel: in place) or a plain <span>, using resolveSectionTarget(target, label?, routes).
Routes come from PAGE_SECTION_ROUTES_TOKEN (default DEFAULT_PAGE_SECTION_ROUTES:
objectsPath: '/obiekty', pagePath: '/strona', dictionaryFilters: { creator: 'creators',
department: 'department', cycle: 'cycles' }, dictionarySources, dictionaryLimit: 50, no
cardTypeFilter):
museum_object→[objectsPath, id]page→[pagePath, id](/strona/:id;pagePathis optional, default/strona)dictionary→[objectsPath]with?filter=[{"name":<filter>,"data":[{"id","value"}]}]whendictionaryFiltersmaps itsdictionaryType(defaults:creator→creators,department→department,cycle→cycles— theFiltersContextfilters); otherwise not clickable; a whole dictionary (dictionary: null, not expanded) is not clickable.taghas no default — the object search has no tag filter (keywordsis a different dictionary); map it once one existscard_type_code→[objectsPath]with one?filter=entry per code (any matches) whencardTypeFilteris set. TODO: the framework object search has no card-type filter yet, so by default these targets are not clickablelink→http(s)://,//,mailto:,tel:external (newTab: truefor the web ones, checked on the control-character-stripped URL like the scheme);#xfragment; other schemes (javascript:,data:, …) and an empty URL not clickable; anything else router commands + query + fragmentmodule(CMS target whose fields the API does not send yet), unknown types,null→ not clickable
providers: [
{
provide: PAGE_SECTION_ROUTES_TOKEN,
useValue: { ...DEFAULT_PAGE_SECTION_ROUTES, objectsPath: '/zbiory' },
},
];Dictionary targets — two modes. In the CMS a block with link type "dictionary record" either picks selected records or a whole dictionary:
- A — selected records (unchanged):
target = { targetType: 'dictionary', dictionaryType, dictionary: <id> }— one block per record, resolved as above. - B — whole dictionary:
target = { targetType: 'dictionary', dictionaryType, dictionary: null }(a missingdictionarytoo —isWholeDictionaryTarget(target)). Contract agreed with MobileMS, pending backend confirmation (until then the API sends such blocks astarget: null, which stay non-clickable blocks).smart-mobilems-page-sectionsreplaces such a block, before dispatch (top-level sections andgrid_containerchildren alike — variants only ever see ordinary mode-A blocks), with one block per dictionary entry:content.titleandtarget.targetLabel= entry name,target.dictionary= entry id, every other field (background, lead, grid…) copied from the source block. Entries keep the API order (no client sorting — the editor's order of the CMS dictionary) and are inserted at the source block's place; the section's blocks getpositionrenumbered0…n-1. Entries already selected in the same section (samedictionaryType+ id — the selected block stays) or produced by an earlier whole-dictionary block are skipped. While loading, after a failed request, and for a type without a source, the block is left out — the rest of the section renders.
Dictionaries are loaded by PageSectionDictionaryExpander (root service, needs HttpClient):
GET {apiUrl}dictionary/<source>/search/page/1?maxPerPage=<limit>&_type=query → data.items[{ id,
name }]. One request per dictionaryType for the whole app (on the server: per request), shared
by every section and renderer; a failed request resolves to null and is retried by the next
renderer. The renderer tracks it with PendingTasks, so SSR waits; a cached / HTTP-transfer-cached
response arrives synchronously, so hydration renders the same blocks.
| dictionaryType | dictionarySources (API dictionary) | dictionaryFilters (object search filter) |
| ---------------- | ------------------------------------ | ------------------------------------------ |
| creator | Creator | creators |
| department | Department | department |
| tag | Tag | — (no tag filter: expanded, not clickable) |
| cycle | Cycle | cycles (FiltersContext.cycles) |
| any other | — (add it to both maps) | — |
PageSectionRoutes.dictionarySources (default DEFAULT_PAGE_SECTION_DICTIONARY_SOURCES, also used
when a museum's routes omit it) maps a type to an API dictionary name or, with a /, a path relative
to apiUrl (e.g. article/authors); dictionaryLimit caps the entries per block (default
DEFAULT_PAGE_SECTION_DICTIONARY_LIMIT, 50). A museum providing its own dictionaryFilters without
cycle gets non-clickable cycle pills — spread DEFAULT_PAGE_SECTION_ROUTES and override.
Outside the renderer (resolver, own component): await expander.expand(sections) (normalized
sections → expanded), expander.entries(type) / entries$(type) (PageSectionDictionaryEntry[]
or null), or the pure wholeDictionaryTypes(sections) + expandDictionaryBlocks(sections,
entries) (PageSectionDictionaryEntries — type → entries | null; toEntriesMap(pairs) builds it
from [type, entries] pairs). A whole-dictionary target
that was not expanded (e.g. expandDictionaries = false) resolves to null in
resolveSectionTarget — never a filter with an undefined id.
Content, media, widgets — helpers for variants (the framework gives data, the museum the look):
| Helper | Returns |
| -------------------------------------------- | --------------------------------------------------------------------------------------------- |
| pageSectionHtml(html) | the CMS HTML, or null when it renders nothing (null, blanks, <p><br></p>, ) |
| smartMobilemsSectionHtml pipe (PageSectionHtmlPipe) | DomSanitizer-sanitized HTML (or null) for [innerHTML] — scripts/handlers stripped, javascript: neutralized; same in SSR |
| pageSectionCtaLabel(cta) | trimmed targetLabel or null — fallback texts per target type are the museum's translations |
| pageSectionMedia(media, multimediaUrl?) | PageSectionMediaInfo { url, type: 'image' \| 'video' \| 'audio', alt, isDecorative } or null without a file |
| pageSectionMediaUrl(media, multimediaUrl?) | just the URL, or null |
| pageSectionFileUrl(filePath, multimediaUrl?) | absolute URLs as-is, others joined with SharedConfig.multimediaUrl (like FileUrlService noCache) |
| gridArea(grid) | PageSectionGridArea { 'grid-column': 'x+1 / span w', 'grid-row': 'y+1 / span h' } clamped to PAGE_SECTION_GRID_COLUMNS (12), or null — for mosaics |
| pageSectionCounterValue(counter, locale) | banner counter value via Intl.NumberFormat(locale) (empty/invalid locale → PAGE_SECTION_FALLBACK_LOCALE 'pl-PL', never the runtime default), or null |
| smartMobilemsSectionCounter pipe (PageSectionCounterPipe) | the same for the app LOCALE_ID — identical in SSR and the browser |
Media kind (PageSectionMediaKind): mp3|wav|m4a → audio; API type: 'video' or webm|mp4 →
video; ogg not marked as video → audio; else image. No/blank altText → decorative
(alt: ''). The counter locale is explicit so SSR and hydration format the same text:
{{ counter | smartMobilemsSectionCounter }} (LOCALE_ID) or
pageSectionCounterValue(counter, inject(TranslationService).currentLanguage()). The banner search
box (widget.search[].placeholder) is rendered by the museum variant.
@Component({
selector: 'app-tiles-section',
imports: [SectionLinkComponent, PageSectionHtmlPipe],
template: `
@if (section().content?.lead | smartMobilemsSectionHtml; as lead) {
<div [innerHTML]="lead"></div>
}
@for (tile of tiles(); track $index) {
<smart-mobilems-section-link [target]="tile.block.target" [label]="tile.block.content?.title ?? null">
@if (tile.media; as media) {
<img [src]="media.url" [alt]="media.alt" />
}
</smart-mobilems-section-link>
}
`,
})
export class TilesSectionComponent extends PageSectionBaseComponent {
private readonly multimediaUrl = inject(SharedConfig).multimediaUrl;
readonly tiles = computed(() =>
this.section().blocks.map((block) => ({
block,
media: pageSectionMedia(block.media?.background, this.multimediaUrl),
})),
);
}Test fixture. TEST_SECTIONS (PageSectionRaw[]) and TEST_SECTIONS_PAGE (Page) are a
verbatim copy of GET page/slug/test-sekcje — every CMS section setting in use (11
tile_collection themes, 4 slider, 5 banner incl. widgets, a grid_container with 3 children,
portal_slot, all target types but module). Use them in museum tests to check that every
registered variant copes with the real data.
MasonryGridComponent
Owns the whole masonry lifecycle: it creates the @thisissoon/angular-masonry instance, waits for
the grid images to load, re-layouts when list changes, destroys the instance, and fades the grid in
once the layout settles. The host only supplies the items template and (once) the Masonry factory.
<smart-mobilems-masonry-grid [list]="list()" [options]="{ gutter: 24 }" classes="smart-my-4">
<ng-template #masonryContentTpl>
<div class="masonry-grid-sizer"></div>
@for (item of list(); track item.id) {
<smart-objects-grid-item [item]="item" />
}
</ng-template>
</smart-mobilems-masonry-grid>The grid item carries masonry-item on its own root element (host class). Never wrap a card
in an extra <div class="masonry-item"> — masonry itemizes matching descendants too, so the
card would be laid out twice, collapse to a fraction of the column width and overlap its
neighbours. The component defensively strips the item class from nested matches (with a
console warning), but the wrapper markup is still wrong — don't write it.
| Input | Type | Default | Notes |
| --------- | -------------------- | ---------------- | ----------------------------------------------------------- |
| list | Array<unknown> | — | Triggers destroy + re-init + re-layout when it changes |
| options | MasonryOptions | {} | Merged over itemSelector: .masonry-item, columnWidth: .masonry-grid-sizer, gutter: 24 |
| classes | string \| string[] | 'smart-my-4' | Applied to the grid container |
Content child: #masonryContentTpl (required). Public methods: initMasonry(), destroyMasonry();
masonryReady signal gates the opacity transition. Gutter drops to 0 below 768 px.
SSR provider (once per lazy route or module that renders a grid):
{
provide: Masonry,
useFactory: () =>
typeof window !== 'undefined' && (window as any)['Masonry']
? (window as any)['Masonry']
: null,
}stripNestedMasonryItems(container, itemSelector) is the helper the component runs before layout: it
removes the item class from elements nested inside another item, so a card can never be laid out
twice. Exported for tests and custom grids; only simple .class selectors are handled.
FiltersBaseComponent / FiltersContext
FiltersContext<T> translates filter UI state into a CrudFilter on the CrudFacade<T>: each
accessor reads the current filter and writes it back through a debounced (500 ms), change-checked
refresh, so bound controls never rebuild the query array themselves.
@Component({ selector: 'smart-mobilems-objects-filters', templateUrl: './filters.component.html' })
export class FiltersComponent extends FiltersBaseComponent<MsObject> {}<ng-template #contentTpl>
<input [(ngModel)]="context.searchText" />
<smart-filter-dictionary-input [(value)]="context.keywords" dictionaryKey="keywords" />
</ng-template>FiltersBaseComponent<T> (selector smart-mobilems-filters) injects CrudFacade<T>, builds
context in ngAfterContentInit, and exposes the #contentTpl content child.
FiltersContext<T> accessors — each reads/writes the facade filter and refreshes it:
- paging/sorting:
searchText,page,limit,sortBy,sortDesc,sort,query,totalCount - flags:
onlyImage,exposure - dictionary/query filters:
creators,authors,keywords,cycles,techniques,materials,category,department,location,locationType,createPlaces,formFeature,targetGroups,template,types,extraNumbersValue,inventoryNumber - date ranges:
createDatesFrom,createDatesTo,publicationDateFrom,publicationDateTo - bulk:
filters— set several{ val, key, type }entries in one refresh
FilterType = string | string[] | MenuItem | MenuItem[] | undefined.
Base components (extend these)
| Base class | Extend for |
| -------------------------------------- | ------------------------------------------------------------------- |
| FiltersBaseComponent<T> | A module's filters component |
| SearchBaseComponent<SEARCH_PATH> | The search page / search panel |
| SliderBaseComponent | Home and detail sliders/carousels (slides: PageSlide[] input) |
| PageStaticComponent | CMS static page ("o nas", "kontakt", …) — loads Page by route id, sets the SEO title |
| GalleryFullscreenBaseComponent | Fullscreen gallery dialogs (closeChange output) |
| MediaTabBaseComponent<MediaType> | Object media tabs (item, formFeatureTypes inputs) |
| WidgetBaseComponent<T> | A content widget (item input) |
| PageSectionBaseComponent | A CMS page section variant (section input, required) |
Services
All are providedIn: 'root' unless noted; SERVICES exports the array for module providers.
| Service | Key API |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| ConfigsService | get(), getPage(ref: PageRef) — CMS config + page fetch (page/<id>, page/home, page/slug/<slug>) |
| DictionaryService<T> | get(...), getNode(...) — filter dictionaries (lowercase keys, e.g. authors) |
| CrudBaseService<T> | abstract: getById(id), downloadPdf(name, id) — extend for a module service |
| SearchService | search(txt, data), setCurrentSearchText(txt), getFilterQuery(...) (needs SEARCH_CONFIG_TOKEN); the standalone getSearchFilterQuery(type, txt, data) builds the same query string outside DI |
| FileUrlService | get(...) with FileUrlMode = 'cache' \| 'big' \| 'noCache' — builds multimedia URLs |
| SettingsService | getMediaUrl(), getBaseTitle(), getCanLink(), getMainLogo() |
| TranslationService | init(...), get(key, params), setLanguage(lang) (ITranslationConfig) |
| SeoService | updateTitle, updateDescription, updateMetaTags, updateOgTags |
| MetaService | keepUpdatingPageTitle, setPageMetadata, setObjectMetadata, setImageMetadata, resetMetadata, setTitle, setDescription, updateTag, destroy |
| WcagService | init(), setContrast/setText/setLetterSpacing/setWordSpacing/setGrayscale, reset() |
| ContrastService | changeTheme(active), toggle() |
| StyleService | init(elementRef, customVariables?) — applies theme CSS variables |
| ModalService | open(templateRef): ModalRef, closeAll(); ModalRef.close() |
| AuthStorageService | setUser, getUser, removeUser, hasUser |
| GlobalService | setHeaderVisibility, setFooterVisibility, hexToRgb, rgbToHex |
| QueryFilterService | Query-string ⇄ filter helpers |
State
| Export | Purpose |
| --------------- | ------------------------------------------------------------------- |
| ConfigsFacade | init(): Promise<Config>, getPage(ref: PageRef), config signals |
Feature state (ObjectsFacade, ArticlesFacade, PublicCollectionsFacade) lives in the feature
packages and extends CrudFacade from @smartsoft001/crud-shell-angular.
Directives
All three are standalone. SharedModule does not re-export them — the exported DIRECTIVES
array is empty, so importing SharedModule gives you no directives. Add the ones you need to the
imports of the standalone component that uses them:
import { ScrollableDirective, HoverDirective } from '@smartsoft001-mobilems/angular';
@Component({ imports: [ScrollableDirective, HoverDirective], /* … */ })| Directive | Selector | Purpose |
| ----------------------- | --------------------- | ---------------------------------------------- |
| ClickOutsideDirective | [smartClickOutside] | Emits ClickOutsideEvent on an outside click |
| HoverDirective | [smartHover] | Hover state without component boilerplate |
| ScrollableDirective | [smartScrollable] | Programmatic scrolling + overflow detection |
ClickOutsideDirective — input isActive: boolean (default true; when false nothing is
emitted), output smartClickOutside: ClickOutsideEvent. The output shares the selector's name, so the
element carries both: <div smartClickOutside [isActive]="open()" (smartClickOutside)="close()">.
Clicks on descendants count as inside. ClickOutsideEvent is { event: MouseEvent; target: EventTarget | null }.
HoverDirective — no inputs or outputs. Adds/removes the hover class on the host element on
mouseenter / mouseleave; style it from CSS.
ScrollableDirective — exportAs: 'smartScrollable', so grab it with a template reference
variable. Required input scrollUnit: number (pixels per scroll unit). Methods
scrollHorizontal(units) / scrollVertical(units) (negative units scroll back). Read-only signals
— call them: canScrollStartHorizontal(), canScrollEndHorizontal(), canScrollStartVertical(),
canScrollEndVertical(), scrollLeftPosition(), scrollTopPosition(). They refresh on scroll,
on window resize, and when scrollUnit changes.
<div smartScrollable [scrollUnit]="200" #scroll="smartScrollable">…</div>
<button (click)="scroll.scrollHorizontal(1)" [disabled]="!scroll.canScrollEndHorizontal()">→</button>Pipes
| Pipe | Class | Purpose |
| -------------------------- | --------------------- | ------------------------------------------------------------ |
| smartMobilemsSectionHtml | PageSectionHtmlPipe | Sanitized CMS section HTML or null (see page sections) |
| smartMobilemsSectionCounter | PageSectionCounterPipe | Banner counter value for the app LOCALE_ID (see page sections) |
Apart from that the framework ships no pipes. If a museum app needs one, add it to that app's
libs/shared/angular (and its README) — never per-module.
Guards
| Guard | Purpose |
| ---------------------- | ------------------------------------------- |
| authenticationGuard | CanActivateFn — requires a stored user |
| unauthorizedGuard | CanActivateFn — only for anonymous users |
Models, interfaces and tokens
Config/UI:
ListMode,IQuickMenu,ITab,IImages,ImageBox,IAudio,IVideo,IErrorMessages,IObjectGallery,IObjectGalleryItem,IObjectGallerySingleObject,ISingleObjectMultimediaGames:
IGame,GameType,IGameQuestion,IGameAnswer,IGameMemoConfig,IGamePuzzleConfig,IGameQuizConfigComponent contracts:
IHeaderUser,IHeaderConfig,IFooterStaticPage,IFooterConfig,IMenuItem,IMenuConfig,ClickOutsideEvent,IDictionaryItem,PageSlide(slider input)Search:
ISearchConfig,ISearchData,ISearchResults,SearchSource,SEARCH_CONFIG_TOKEN,SearchQuery, and the saved-query shapesIUserQuery,IUserQueryList,IFilterItem,IFilterOneAccount flows (used by apps that enable the user module):
IUser,IUserInfo,IUserRegister,IUserModify,ILoginResponse, and the form error messagesChangePasswordErrorMessage,ForgotPasswordErrorMessage,ModifyUserErrorMessage,RegisterErrorMessageName collision.
@smartsoft001-mobilems/modelsdeclares its ownIUser,IUserInfo,IUserRegister,IUserModifyandILoginResponse, and the shapes are not identical — e.g. this package'sIUserInfois{ email, userID, avatar?, newsletterAcceptance }while the models one is{ username?, email, [key: string]: any }, andIUser.refreshTokenis optional here but required there. They are not interchangeable. Use the ones from this package in Angular UI code (the components and services are typed against them), and import the models variants only when working with API payloads. Never mix the two in one type position.Widgets:
ArticlesWidgetComponentsToken,ARTICLES_WIDGET_COMPONENTS_TOKENPage sections (see
PageSectionsComponent):PAGE_SECTION_COMPONENTS_TOKEN,PageSectionComponentsToken,pageSectionComponent,PAGE_SECTION_PLACEHOLDER_TYPE,PAGE_SECTION_GRID_CONTAINER_TYPE,PAGE_SECTION_HOOK_CLASS,PAGE_SECTION_SLOT_CLASS,PAGE_SECTION_ROUTES_TOKEN,PageSectionRoutes,DEFAULT_PAGE_SECTION_ROUTES,resolveSectionTarget,ResolvedSectionTarget,DEFAULT_PAGE_SECTION_DICTIONARY_SOURCES,DEFAULT_PAGE_SECTION_DICTIONARY_LIMIT,PageSectionDictionaryExpander,PageSectionDictionaryEntry,PageSectionDictionaryEntries,isWholeDictionaryTarget,wholeDictionaryTypes,expandDictionaryBlocks,toEntriesMap,normalizePageSections,pageSectionAppearance,PageSectionAppearanceStyle,PageSectionBackgroundKind,PAGE_SECTION_SPACE_MAX,pageSectionFileUrl,pageSectionHtml,pageSectionCtaLabel,pageSectionCounterValue,PageSectionCounterPipe,PAGE_SECTION_FALLBACK_LOCALE,pageSectionMedia,pageSectionMediaUrl,PageSectionMediaInfo,PageSectionMediaKind,gridArea,PageSectionGridArea,PAGE_SECTION_GRID_COLUMNS, the fixturesTEST_SECTIONS/TEST_SECTIONS_PAGE, andPageRef(number | 'home' | `slug/${string}`, thegetPageargument)WCAG:
IWcagConfig,WcagContrast,WcagText,WcagLetterSpacing,WcagWordSpacing,WcagGrayscale,WcagChangeTypeEnvironment:
environment(only the base build is exported from the package entry point;environment.prod.tsis not reachable through the barrel)
Entities (Config, Page, PageSection*, MsFile, IUser, the IArticle* widget interfaces, MenuItem, …) come
from @smartsoft001-mobilems/models.
Module & config
SharedModule.forRoot(config)— root: registers the components and bootsConfigsFacade. It exports no directives — import those standalone (see Directives).SharedModule.forFeature(config)/SharedModule— feature libraries.SharedConfig—apiUrl,multimediaUrl,mainLogoPath,styleVariables.
Bootstrap entry points
Two secondary entry points replace the boilerplate of main.ts / main.server.ts. They are
split on purpose: masonry-layout touches window at import time and would crash SSR, so the
server entry never imports it.
bootstrapApplication(rootComponent, options?)from@smartsoft001-mobilems/angular/bootstrap— browser only. Registersmasonry-layoutonwindow.Masonry(consumed by theMasonrytoken factories of@thisissoon/angular-masonry) and returns Angular'sPromise<ApplicationRef>. Requiresmasonry-layout(peer dependency) andallowedCommonJsDependencies: ["masonry-layout"]in the app's build options.bootstrapServerApplication(rootComponent, options)from@smartsoft001-mobilems/angular/bootstrap/server— returns theServerBootstrapfactory ((context: BootstrapContext) => Promise<ApplicationRef>) thatmain.server.tsexports as default. Anything that must run before the app modules are evaluated (environment init fromprocess.env) still has to be imported first inmain.server.ts.
// main.ts
import { bootstrapApplication } from '@smartsoft001-mobilems/angular/bootstrap';
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
// main.server.ts
import './env-init';
import { bootstrapServerApplication } from '@smartsoft001-mobilems/angular/bootstrap/server';
export default bootstrapServerApplication(App, config);SSR server (ssr)
@smartsoft001-mobilems/angular/ssr (Node only) replaces the hand-written Express server.ts.
Peers: @angular/ssr, express (4 or 5), dotenv.
createMobilemsServer(options)— assembles dotenv, the SSRF allow-list (hosts ofSITE_URL/MULTIMEDIA_URL/API_URL+localhostoutside production),GET /env.js,GET /api/translations,GET /robots.txt,GET /sitemap.xml, yourconfigure(app)routes, static files and the Angular request handler; listens when the module is the entry point. Returns{ app, angularApp, reqHandler, browserDistFolder }— exportreqHandlerfromserver.ts.MobilemsServerOptions—moduleUrl(import.meta.url),sitemap(SitemapOptions:routes: SitemapRoute[],languages,defaultLanguage,langQueryParam),robots(RobotsOptions:disallow;falsedisables),env(EnvMap: extrawindow.envkeys → env var name or resolver, seeEnvValueSource),translations(TranslationsOptions: candidatepaths;falsedisables),allowedHosts,allowedHostsEnvVars,trustProxyHeaders(defaultDEFAULT_TRUST_PROXY_HEADERS),dotenv,configure,autoListen,port.SitemapChangefreqlists the acceptedchangefreqvalues;MobilemsServeris the return type.- Building blocks, for a custom server:
buildAllowedHosts(BuildAllowedHostsOptions,DEFAULT_ALLOWED_HOSTS_ENV_VARS),createEnvScriptHandler/resolveEnvValues/renderEnvScript(DEFAULT_ENV_MAP=apiUrl,siteUrl,multimediaUrl,version,useLocalTranslations; values are JSON-encoded),createSitemapHandler/buildSitemapXml/localizedUrl(DEFAULT_SITEMAP_LANGUAGES,DEFAULT_SITEMAP_LANGUAGE),createRobotsHandler/buildRobotsTxt(BuildRobotsTxtOptions),createTranslationsHandler/defaultTranslationPaths,resolveSiteUrl/withProtocol/hostnameOf(SiteUrlRequest).
// server.ts
import { createMobilemsServer } from '@smartsoft001-mobilems/angular/ssr';
export const { reqHandler } = createMobilemsServer({
moduleUrl: import.meta.url,
sitemap: {
routes: [
{ path: '/strona-glowna', changefreq: 'daily', priority: 1 },
{ path: '/obiekty', changefreq: 'daily', priority: 0.9 },
],
},
robots: { disallow: ['/article-preview', '/wyszukaj'] },
env: { gtmId: 'GTM_ID' },
});Translations
setTranslationsAndLang(translateService) seeds the bundled PL strings shipped with the package
(TRANSLATE_DATA_PL, typed by ITranslateData) into an ngx-translate TranslateService and sets
the active language.
