npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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; pagePath is optional, default /strona)
  • dictionary → [objectsPath] with ?filter=[{"name":<filter>,"data":[{"id","value"}]}] when dictionaryFilters maps its dictionaryType (defaults: creator → creators, department → department, cycle → cycles — the FiltersContext filters); otherwise not clickable; a whole dictionary (dictionary: null, not expanded) is not clickable. tag has no default — the object search has no tag filter (keywords is a different dictionary); map it once one exists
  • card_type_code → [objectsPath] with one ?filter= entry per code (any matches) when cardTypeFilter is set. TODO: the framework object search has no card-type filter yet, so by default these targets are not clickable
  • link → http(s)://, //, mailto:, tel: external (newTab: true for the web ones, checked on the control-character-stripped URL like the scheme); #x fragment; other schemes (javascript:, data:, …) and an empty URL not clickable; anything else router commands + query + fragment
  • module (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 missing dictionary too — isWholeDictionaryTarget(target)). Contract agreed with MobileMS, pending backend confirmation (until then the API sends such blocks as target: null, which stay non-clickable blocks). smart-mobilems-page-sections replaces such a block, before dispatch (top-level sections and grid_container children alike — variants only ever see ordinary mode-A blocks), with one block per dictionary entry: content.title and target.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 get position renumbered 0…n-1. Entries already selected in the same section (same dictionaryType + 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>, &nbsp;) | | 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, ISingleObjectMultimedia

  • Games: IGame, GameType, IGameQuestion, IGameAnswer, IGameMemoConfig, IGamePuzzleConfig, IGameQuizConfig

  • Component 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 shapes IUserQuery, IUserQueryList, IFilterItem, IFilterOne

  • Account flows (used by apps that enable the user module): IUser, IUserInfo, IUserRegister, IUserModify, ILoginResponse, and the form error messages ChangePasswordErrorMessage, ForgotPasswordErrorMessage, ModifyUserErrorMessage, RegisterErrorMessage

    Name collision. @smartsoft001-mobilems/models declares its own IUser, IUserInfo, IUserRegister, IUserModify and ILoginResponse, and the shapes are not identical — e.g. this package's IUserInfo is { email, userID, avatar?, newsletterAcceptance } while the models one is { username?, email, [key: string]: any }, and IUser.refreshToken is 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_TOKEN

  • Page 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 fixtures TEST_SECTIONS / TEST_SECTIONS_PAGE, and PageRef (number | 'home' | `slug/${string}`, the getPage argument)

  • WCAG: IWcagConfig, WcagContrast, WcagText, WcagLetterSpacing, WcagWordSpacing, WcagGrayscale, WcagChangeType

  • Environment: environment (only the base build is exported from the package entry point; environment.prod.ts is 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 boots ConfigsFacade. 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. Registers masonry-layout on window.Masonry (consumed by the Masonry token factories of @thisissoon/angular-masonry) and returns Angular's Promise<ApplicationRef>. Requires masonry-layout (peer dependency) and allowedCommonJsDependencies: ["masonry-layout"] in the app's build options.
  • bootstrapServerApplication(rootComponent, options) from @smartsoft001-mobilems/angular/bootstrap/server — returns the ServerBootstrap factory ((context: BootstrapContext) => Promise<ApplicationRef>) that main.server.ts exports as default. Anything that must run before the app modules are evaluated (environment init from process.env) still has to be imported first in main.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 of SITE_URL / MULTIMEDIA_URL / API_URL + localhost outside production), GET /env.js, GET /api/translations, GET /robots.txt, GET /sitemap.xml, your configure(app) routes, static files and the Angular request handler; listens when the module is the entry point. Returns { app, angularApp, reqHandler, browserDistFolder } — export reqHandler from server.ts.
  • MobilemsServerOptions — moduleUrl (import.meta.url), sitemap (SitemapOptions: routes: SitemapRoute[], languages, defaultLanguage, langQueryParam), robots (RobotsOptions: disallow; false disables), env (EnvMap: extra window.env keys → env var name or resolver, see EnvValueSource), translations (TranslationsOptions: candidate paths; false disables), allowedHosts, allowedHostsEnvVars, trustProxyHeaders (default DEFAULT_TRUST_PROXY_HEADERS), dotenv, configure, autoListen, port. SitemapChangefreq lists the accepted changefreq values; MobilemsServer is 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.