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

@mmlogic/components

v0.3.2

Published

Stencil.js web component library for dynamic forms and virtual-scroll data tables

Readme

@mmlogic/components

A Stencil.js v4 web component library for rendering dynamic forms and paginated data tables from API descriptors. Components are framework-agnostic (plain HTML/JS) and an Angular output target is included.

Installation

npm install @mmlogic/components

Usage

Vanilla HTML / JS

<link rel="stylesheet" href="node_modules/@mmlogic/components/dist/mosterdcomponents/mosterdcomponents.css" />
<script type="module" src="node_modules/@mmlogic/components/dist/mosterdcomponents/mosterdcomponents.esm.js"></script>

<mrd-form id="my-form"></mrd-form>

<script>
  const form = document.getElementById('my-form');
  form.layout = { /* ClientLayout descriptor */ };
  form.values = { name: 'Alice' };
  form.addEventListener('mrdSubmit', e => console.log(e.detail));
</script>

Via CDN (unpkg)

<link rel="stylesheet" href="https://unpkg.com/@mmlogic/components/dist/mosterdcomponents/mosterdcomponents.css" />
<script type="module" src="https://unpkg.com/@mmlogic/components/dist/mosterdcomponents/mosterdcomponents.esm.js"></script>

Angular

// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { defineCustomElements } from '@mmlogic/components/loader';

defineCustomElements();

@NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })
export class AppModule {}

Components

<mrd-form>

Renders a complete form from a layout descriptor. Handles validation, RTL layout (Arabic locale), and orchestrates file uploads, relation search, and dependent dropdowns.

<mrd-form id="form" locale="nl-NL" show-cancel></mrd-form>
const form = document.getElementById('form');

form.layout = {
  title: 'New invoice',
  items: [
    { type: 'FIELD', field: { name: 'description', label: 'Description', dataType: 'TEXT', required: true } },
    { type: 'FIELD', field: { name: 'amount', label: 'Amount', dataType: 'CURRENCY' } },
  ]
};

form.addEventListener('mrdSubmit', e => console.log(e.detail));
form.addEventListener('mrdCancel', () => history.back());

Props

| Prop | Type | Default | Description | |---|---|---|---| | layout | ClientLayout | required | Form descriptor with title and items[] | | values | Record<string, any> | {} | Initial field values; re-setting this prop re-fills the form (edit mode) | | locale | string | navigator.language | BCP 47 locale for labels, validation messages, and formatting | | referenceHref | string | '' | Absolute href of a parent/context object. Combined with referenceClass, mrd-form automatically pre-fills the matching relation field so dependent dropdowns load without any host-side form logic. | | referenceClass | string | '' | mostSignificantClass of the parent object (e.g. 'clientAgreements'). Used to locate the relation field to pre-fill. | | showCancel | boolean | false | When true, shows a Cancel button next to Submit in the footer. |

Events

| Event | Detail | Description | |---|---|---| | mrdSubmit | Record<string, any> | Fired on valid submit. Payload contains only layout fields; relation values are normalised to plain href strings. | | mrdCancel | — | Fired when the Cancel button is clicked. Host handles navigation. | | mrdSearch | { query: string; relatedClass: string } | Relation search requested. Host calls field.setSearchResults(results) with the API response. | | mrdFetchAll | see below | All records requested for a DROPDOWN relation. Host calls field.setAllRecords(results). | | mrdUpload | { name: string; file: File } | A file was selected. Host uploads the file and calls form.setFieldValue(name, uri) when done. The field shows a loading spinner until the URI arrives. |

Methods

| Method | Description | |---|---| | setFieldValue(name, value) | Inject a value into a named field from outside (e.g. after a file upload completes). | | setSearchResults(results) | (on mrd-relation-field) Populate the search autocomplete list. | | setAllRecords(results) | (on mrd-relation-field) Populate a DROPDOWN <select> with all records. |

mrdFetchAll event detail

{
  name: string;                  // field name — use to find the mrd-relation-field element
  relatedClass: string;          // dot-notation class (e.g. "legalmanager.budget")
  mostSignificantClass?: string; // URL segment (e.g. "budgets")
  commonRelation?: string;       // name of the dependency field (for dependent dropdowns)
  filter?: string;               // query param name
  filterValue?: string;          // href of the selected dependency; empty string = clear list
}

Full integration example

// mrdSearch — typeahead for RELATION fields
form.addEventListener('mrdSearch', async (e) => {
  const { query, relatedClass } = e.detail;
  const data = await fetch(`/api/search/${relatedClass}?q=${query}`).then(r => r.json());
  const field = form.querySelector(`mrd-relation-field[name="${relatedClass}"]`);
  field?.setSearchResults(data.map(r => ({ id: r._links.self.href, label: r.name })));
});

// mrdFetchAll — populate DROPDOWN selects (including dependent dropdowns)
form.addEventListener('mrdFetchAll', async (e) => {
  const { name, mostSignificantClass, filter, filterValue } = e.detail;
  const field = form.querySelector(`mrd-relation-field[name="${name}"]`);
  if (!field) return;

  if (filter && !filterValue) {       // dependency cleared → empty the list
    field.setAllRecords([]);
    return;
  }

  let url = `/data/${tenantId}/${mostSignificantClass}`;
  if (filter && filterValue) url += `?${filter}=${encodeURIComponent(filterValue)}`;
  const result = await fetch(url).then(r => r.json());
  const records = Object.values(result._embedded ?? {})[0] ?? [];
  field.setAllRecords(records.map(r => ({ id: r._links.self.href, label: r.name })));
});

// mrdUpload — stream file to server before submit
form.addEventListener('mrdUpload', async (e) => {
  const { name, file } = e.detail;
  const formData = new FormData();
  formData.append('file', file);

  // Get pre-signed upload URL from server, then POST the file
  const uploadUrl = await fetch('/api/upload').then(r => r.json());
  const [uri] = await fetch(uploadUrl, { method: 'POST', body: formData }).then(r => r.json());

  // Return the stored URI to the field — spinner disappears, value is ready for submit
  form.setFieldValue(name, uri);
});

<mrd-table>

A virtual-scroll paginated table with column sorting, Excel-style filtering, toolbar actions, and a pagination footer. Only visible rows are in the DOM; pages are fetched on demand via an event.

<mrd-table id="table"></mrd-table>
const table = document.getElementById('table');

table.columns       = view.columns;        // TableColumn[]
table.totalElements = page.totalElements;
table.pageSize      = page.size;
table.tableHeight   = 500;                 // px, fixed container height
table.locale        = 'nl-NL';            // en / nl / ar / fr
table.defaultSort   = view.defaultSort ?? '';
table.actions       = [
  { action: 'create', label: 'New record',        icon: 'assets/sprites.svg#icon-plus',       variant: 'primary' },
  { action: 'export', label: 'Export to Excel',   icon: 'assets/sprites.svg#icon-file-excel', disabled: true },
];

let activeFilters = [];

table.addEventListener('mrdFilter', (e) => {
  activeFilters = e.detail.filters;   // ColumnFilter[]
});

table.addEventListener('mrdLoadPage', async (e) => {
  const { page, sort } = e.detail;
  const params = new URLSearchParams();
  if (page > 0) params.set('page', String(page));
  if (sort)     params.set('sort', sort);
  activeFilters.forEach(f => {
    if (f.operator === 'isEmpty')     { params.set(f.field, '');                   return; }
    if (f.operator === 'isNotEmpty')  { params.set(f.field + '_notempty', 'true'); return; }
    if (f.values?.length)             { params.set(f.field, f.values.join(','));    return; }
    if (f.value  != null) params.set(f.field,           String(f.value));
    if (f.from   != null) params.set(f.field + '_from',  String(f.from));
    if (f.to     != null) params.set(f.field + '_to',    String(f.to));
  });
  const qs     = params.toString();
  const result = await fetch(qs ? `${dataUrl}?${qs}` : dataUrl).then(r => r.json());
  const rows   = Object.values(result._embedded ?? {})[0] ?? [];
  await table.setPage(page, rows);
});

table.addEventListener('mrdAction', (e) => {
  if (e.detail.action === 'create') location.href = '/new';
  if (e.detail.action === 'export') window.open(excelUrl, '_blank');
});

table.addEventListener('mrdRowClick', (e) => {
  location.href = e.detail._links.self.href;
});

await table.init();
await table.setPage(0, page0Rows); // inject pre-fetched page 0 without a second request

Props

| Prop | Type | Default | Description | |---|---|---|---| | columns | TableColumn[] | [] | Column definitions | | totalElements | number | 0 | Total record count. 0 = non-paginated mode (use rows prop instead) | | pageSize | number | 20 | Records per page (must match API page size) | | rowHeight | number | 36 | Row height in px | | tableHeight | number | 500 | Fixed scroll-container height in px | | locale | string | navigator.language | Locale for cell formatting and built-in labels (en/nl/ar/fr) | | defaultSort | string | '' | Initial sort, e.g. "timestamp,desc" or "name" | | rows | Record[] | [] | Rows for non-paginated mode | | actions | TableAction[] | [] | Toolbar action buttons ({ action, label, icon?, variant?, disabled? }) |

Events

| Event | Detail | Description | |---|---|---| | mrdLoadPage | { page: number; sort: string } | Fired when a page needs to be fetched | | mrdRowClick | row object | Fired when a data row is clicked | | mrdAction | { action: string } | Fired when a toolbar action button is clicked | | mrdFilter | { filters: ColumnFilter[] } | Fired when active column filters change |

Methods

| Method | Description | |---|---| | init() | Reset state and render window. Call after all props are set and before setPage(0, …). | | setPage(page, rows) | Inject fetched rows for a page. Automatically clamps the render window when the page is shorter than pageSize — prevents shimmer rows beyond actual data. |

Sort format: "fieldName" for ASC, "fieldName,desc" for DESC — append directly as &sort=<value>.

Column filtering

A filter-toggle button (▼) in the toolbar activates filter mode. Clicking ▾ in any column header opens a popup with sort and type-specific filter controls. Supported per data type:

| DataType | Filter | |---|---| | TEXT, EMAIL, HYPERLINK, RELATION | Starts with / equals / is empty / is not empty | | INTEGER, DECIMAL, PERCENTAGE, CURRENCY | Exact value or from–to range | | DATE, DATETIME, TIME | Exact date or from–to range | | BOOLEAN | All / Yes / No | | LIST | Checkbox list | | FILE, IMAGE | Not supported |


Field components

All leaf components can be used standalone. Each emits mrdChange and mrdBlur.

| Component | DataType | Description | |---|---|---| | <mrd-text-field> | TEXT | Single-line text input | | <mrd-textarea-field> | TEXTBLOCK | Rich text editor (Quill, lazy-loaded) | | <mrd-number-field> | INTEGER, DECIMAL, PERCENTAGE | Numeric input | | <mrd-currency-field> | CURRENCY | Amount + ISO-4217 currency selector | | <mrd-boolean-field> | BOOLEAN | Toggle switch | | <mrd-date-field> | DATE | Date picker | | <mrd-datetime-field> | DATETIME | Date + time picker | | <mrd-time-field> | TIME | Time picker | | <mrd-email-field> | EMAIL | Email input with validation | | <mrd-hyperlink-field> | HYPERLINK | URL input with validation | | <mrd-list-field> | LIST | Single dropdown or multi-checkbox | | <mrd-file-field> | FILE | Drag-and-drop file upload with upload spinner | | <mrd-image-field> | IMAGE | Drag-and-drop image upload with local preview and spinner | | <mrd-relation-field> | RELATION | Typeahead search (event-driven) or DROPDOWN select |

Common props

| Prop | Type | Description | |---|---|---| | name | string | Field name, echoed in event detail | | label | string | Visible label | | value | any | Current value | | required | boolean | Validates on blur; shows error when empty | | disabled | boolean | Disables the input | | locale | string | BCP 47 locale |

Common events

| Event | Detail | Description | |---|---|---| | mrdChange | { name: string; value: any } | Fired on every value change | | mrdBlur | { name: string; value: any } | Fired on blur (triggers validation) | | mrdUpload | { name: string; file: File } | (file/image only) Fired immediately when a file is selected so the host can stream it to the server |

File / image upload flow

When a file is picked:

  1. The field emits mrdUpload with the raw File object.
  2. The field immediately shows a spinner and updates its value to the File (blocking submit).
  3. The host uploads the file and calls form.setFieldValue(name, uri) (or field.value = uri).
  4. The field detects the string URI, clears the spinner, and is ready for submit.

The submit payload always contains the final URI string — never a File object.


Theming

All design tokens are CSS custom properties. The default primary colour is green (#16a34a). Override on :root or any ancestor to match your brand:

:root {
  --mrd-color-primary:       #16a34a;   /* green-600 — change to your brand colour */
  --mrd-color-primary-light: #dcfce7;
  --mrd-color-primary-dark:  #15803d;
  --mrd-color-primary-hover: #166534;
  --mrd-shadow-focus:        0 0 0 3px rgb(22 163 74 / 0.2); /* match primary */

  --mrd-color-neutral-100:   #f3f4f6;
  --mrd-border-radius:       0.375rem;
  --mrd-font-size-sm:        0.875rem;
}

The full list is defined in src/global/variables.scss and exposed in the built file dist/mosterdcomponents/mosterdcomponents.css.


TypeScript types

import type { ClientLayout, ClientLayoutItem, RelationSearchResult } from '@mmlogic/components';
import type { TableColumn, TableAction, ColumnFilter } from '@mmlogic/components/dist/types/utils/cell-renderer';

License

MIT