Arrow keys: ← previous · → next 33 of 35
@nodedk/ui
0.6.10 experimental

ui#

Copyable server-rendered components, CSS, and browser scripts for NodeDK apps.

Install#

npm i @nodedk/ui

Copy the components and assets into the application. Components are designed for a shared directory because layouts may import neighboring components.

mkdir -p app/components/ui app/assets/css/ui app/assets/js/ui app/assets/img/ui
find node_modules/@nodedk/ui/lib -path '*/components/*.js' \
  -exec cp {} app/components/ui/ \;
find node_modules/@nodedk/ui/lib -path '*/assets/css/*' -type f \
  -exec cp {} app/assets/css/ui/ \;
find node_modules/@nodedk/ui/lib -path '*/assets/js/*' -type f \
  -exec cp {} app/assets/js/ui/ \;
find node_modules/@nodedk/ui/lib -path '*/assets/img/*' -type f \
  -exec cp {} app/assets/img/ui/ \;

Add only the assets needed by your components to the existing bundle:

// app/config/bundle.js
module.exports = {
  css: [
    '/css/ui/variables.css',
    '/css/ui/system.css',
    '/css/ui/banner.css',
    '/css/ui/grid.css',
    '/css/ui/griditem.css',
    '/css/ui/textblock.css',
    '/css/ui/gallery.css'
  ],
  js: ['/js/ui/gallery.js']
}

Load bundles from your layout with state.style(['/bundle.css']) and state.script(['/bundle.js']). Asset images keep the /img/ui/ paths used by the components. Optional base styles include dark.css and colorize.css.

Usage#

// app/pages/home.js
var { html } = require('@nodedk/page')
var Container = require('../layouts/container.js')
var Banner = require('../components/ui/banner.js')
var Grid = require('../components/ui/grid.js')
var Griditem = require('../components/ui/griditem.js')
var Textblock = require('../components/ui/textblock.js')
var Gallery = require('../components/ui/gallery.js')

module.exports = {
  name: 'home',
  paths: '/',
  methods: 'GET',
  handler: function (state) {
    return Container(state, { title: 'Home' }, [
      Banner({ text: 'New collection', label: 'Browse', href: '/collection' }),
      Grid({}, [
        Griditem({}, [
          Textblock({ content: html`<h2>Made for everyday use</h2>` })
        ]),
        Griditem({}, [
          Gallery({
            images: [
              { asset: { url: '/img/front.jpg' } },
              { asset: { url: '/img/detail.jpg' } }
            ]
          })
        ])
      ])
    ])
  }
}

Components are ordinary functions returning HTML strings. They take props = {}; container components also take slot = [], an array of rendered HTML strings. Unlike scaffold layouts, these functions do not take state as their first argument. Use the copied files as application code and adapt interactive behavior to your handlers.

Properties described as HTML are inserted unchanged; supply trusted rendered content. Other text is escaped unless a component’s source states otherwise. Omitted optional content generally renders empty or is left out.

Content components#

Banner({ text, label = '', href }) renders a notice and optional link. Announcement({ message, label = '', href }) adds a close button that removes the announcement from the page. Each uses its matching CSS file.

Banner({ text: 'Orders ship on Monday', href: '/shipping', label: 'Details' })
Announcement({
  message: 'Registration is open',
  href: '/signup',
  label: 'Join'
})

Hero#

Hero({ color, overlay, backgroundImage, headline, subheadline, links }): color is a CSS class; overlay also applies it to the overlay when true. backgroundImage.asset.url supplies the image. headline is text and subheadline is HTML. Each link accepts href, label, and style classes.

Hero({
  color: 'primary',
  overlay: true,
  backgroundImage: { asset: { url: '/img/hero.jpg' } },
  headline: 'Build something useful',
  subheadline: '<p>Start with a small, complete application.</p>',
  links: [{ href: '/start', label: 'Get started', style: 'primary' }]
})

Cards and Pricing#

Cards({ cards }) accepts cards with href, image.asset.url, text (HTML), and tag (text). Images render inside links, so an image also needs href.

Pricing({ plans }) accepts name, badge, info, price, features (array of text), href, and label per plan. Without href, the label is plain text. Use a string such as '0' to display a zero price.

Cards({
  cards: [
    {
      href: '/product',
      image: { asset: { url: '/img/product.jpg' } },
      text: '<h3>Everyday notebook</h3>',
      tag: 'New'
    }
  ]
})
Pricing({
  plans: [
    {
      name: 'Starter',
      badge: 'Popular',
      info: 'For small teams',
      price: '29',
      features: ['Three projects', 'Email support'],
      href: '/signup',
      label: 'Start'
    }
  ]
})

Textblock, Markup, Credits, and Quote#

  • Textblock({ content = '' }): HTML inside .textblock-content.
  • Markup({ content = '' }): HTML inside .markup-content.
  • Credits({ content = '' }): escaped text inside .credits-content.
  • Quote({ text, author }): quote HTML and escaped author text.
Textblock({ content: '<p>A short introduction.</p>' })
Markup({ content: '<h2>Details</h2><p>Complete specifications.</p>' })
Credits({ content: 'Photography by Ada' })
Quote({ text: '<p>Simple to use.</p>', author: 'Kim' })

Testimonial#

Testimonial({ logo, text, avatar, name, role }) renders quote HTML and author text. logo and avatar accept { asset: { url }, alt }.

Testimonial({
  logo: { asset: { url: '/img/company.svg' }, alt: 'Company' },
  text: '<p>We shipped our first version in a week.</p>',
  avatar: { asset: { url: '/img/ada.jpg' }, alt: 'Ada' },
  name: 'Ada',
  role: 'Developer'
})

Faq and Infobox#

Faq({ items }) accepts { question, answer } entries. Questions are text; answers are HTML. Clicking a question toggles its item’s .open class. Requires faq.css and /img/ui/faq-chevron.svg.

Infobox({ title, intro, toggle, details }) accepts title and toggle text, and intro/details HTML. details renders inside a native <details> only when toggle is supplied.

Faq({
  items: [{ question: 'Where do I start?', answer: '<p>Read the guide.</p>' }]
})
Infobox({
  title: 'Delivery',
  intro: '<p>Ships within two days.</p>',
  toggle: 'More details',
  details: '<p>Tracking is included.</p>'
})

Highlight#

Highlight({ content, lang = 'sh' }) builds a fenced code block. The copied component currently imports markdown from @nodedk/page, which does not export it. Adapt that import to a parser’s block() function from markdown before using this template. Include highlighting styles as well as highlight.css.

Media components#

Image({ src, alt, loading = 'lazy' }) renders an image or '' without src. alt defaults to the filename before its first dot.

Gallery({ images }) accepts an array of { asset: { url } } and renders slides, arrows, and dots. Include gallery.css and gallery.js; the script initializes galleries on DOMContentLoaded. It exposes no public methods.

Image({ src: '/img/photo.jpg', alt: 'A notebook on a desk', loading: 'eager' })
Gallery({
  images: [
    { asset: { url: '/img/one.jpg' } },
    { asset: { url: '/img/two.jpg' } }
  ]
})

Video#

Video({ title, info, url, poster, caption }) renders YouTube/Vimeo embeds or native .mp4, .ogg, and .webm files. info is HTML; title and caption are text. poster applies to native video. Use a full YouTube watch?v= URL or an embed URL; shortened youtu.be URLs are not expanded.

Video({
  title: 'Product tour',
  info: '<p>A two-minute introduction.</p>',
  url: '/video/tour.mp4',
  poster: '/img/tour.jpg',
  caption: 'Product tour'
})

Googlemap#

Googlemap({ zoom = 4, locations = [], position = {}, width = '100%', height = '400px' }) renders map containers. It assigns a generated UUID to props.key in place. position is { lat, lng }; an empty position uses the visitor’s location. width and height are CSS dimensions. A falsy zoom uses 4.

Each location accepts latitude, longitude, label, address, postcode, postarea, optional href, and optional hrefLabel (defaults to href).

Googlemap({
  zoom: 12,
  position: { lat: 59.91, lng: 10.75 },
  locations: [
    {
      latitude: 59.91,
      longitude: 10.75,
      label: 'Oslo',
      address: 'Example street 1',
      postcode: '0150',
      postarea: 'Oslo',
      href: '/locations/oslo',
      hrefLabel: 'View location'
    }
  ]
})

Include googlemap.css, googlemap.js, and its icon. The browser script exposes async window.initMaps() with no arguments or result. It requires:

  • Google Maps JavaScript API loaded with callback=initMaps and an API key.
  • Global esc, supplied by @nodedk/browser, loaded first.
  • window.userip and an application POST /request/ip endpoint accepting { query: { ip } } and returning { lat, lon }.
  • A suitable Google map ID in the copied script, whose current value is 'maps'.

Locations are listed by distance from the visitor. Markers navigate to href when supplied. The initialization helper always performs the IP lookup, even with an explicit map position.

Linkrow, Navigation, and Pillars#

Linkrow({ links }) renders { href, label } links. Navigation({ links }) also accepts per-link style classes.

Pillars({ pillars }) supports three item types:

  • { type: 'links', caption, links: [{ href, label }] }.
  • { type: 'info', info } with HTML.
  • { type: 'signature', signature } with HTML.
Linkrow({ links: [{ href: '/about', label: 'About' }] })
Navigation({ links: [{ href: '/start', label: 'Start', style: 'primary' }] })
Pillars({
  pillars: [
    {
      type: 'links',
      caption: 'Company',
      links: [{ href: '/about', label: 'About' }]
    },
    { type: 'info', info: '<p>Oslo, Norway</p>' },
    { type: 'signature', signature: '<p>Made by our team</p>' }
  ]
})

Socialmedia and Stats#

Socialmedia({ accounts }) accepts { url, platform, icon: { asset: { url } } } entries. Links open in a new tab. The current stats.js file exports the same social-media component and accepts accounts; it has no statistics API.

Socialmedia({
  accounts: [
    {
      url: 'https://github.com/nodedk',
      platform: 'GitHub',
      icon: { asset: { url: '/img/github.svg' } }
    }
  ]
})

Burger and Ingress#

Burger({ text = '', click }) renders a menu icon and label. click is an inline JavaScript string, not a function callback.

Burger({
  text: 'Menu',
  click: "document.querySelector('nav').classList.toggle('open')"
})

Ingress({ logo, links, searchAction, searchPlaceholder, align, burgerText }) renders a header. logo.asset.url links to /. align accepts 'left', 'center', or 'right'. Links use { href, label }, or { label, submenu } with submenu entries { route, label }. A search form appears with searchAction, submitting query field s; placeholder defaults to ''.

The mobile template emits a <burger> element and submenu buttons use fx-toggle. Replace these with calls to the copied Burger function and application event handlers; this package does not expand custom component HTML tags or implement fx-* attributes.

Sidebar({ links, groups, menuText = 'Menu' }) accepts { href, label } links and { caption, links } groups. Each group needs a links array. The mobile multiple-link branch reads $.req.pathname; adapt it to receive the current path from your application. Its toggles also use fx-toggle. Requires sidebar.css and both sidebar icons.

Forms and containers#

Field#

Field(props = {}) generates an ID with crypto.randomUUID() when absent, mutating props.id. Its supported properties are:

  • id, name, label, info, value, placeholder, autocomplete.
  • type: default 'text'; also color, date, datetime-local, time, week, month, email, number, password, search, url, tel, textarea, hidden, submit, file, radio, checkbox, and select.
  • required: adds a visual asterisk to ordinary field labels; does not add the HTML required attribute.
  • links: submit-field links with href and label. Submit text defaults to 'Submit'.
  • action, loader: file-field data attributes.
  • legend, options: radio/checkbox groups. Each option supplies its own name, value, and label.
  • options: select entries with value and label. Selection/checked state is not inferred from props.value.

Most text values default to ''; falsy ordinary field values, including 0, also render empty. Date fields use the date helper from @nodedk/page. Unsupported field types render no controls.

Field({ name: 'email', type: 'email', label: 'Email', autocomplete: 'email' })
Field({ name: 'message', type: 'textarea', label: 'Message', value: 'Hello' })
Field({
  name: 'size',
  type: 'select',
  options: [
    { value: 'small', label: 'Small' },
    { value: 'large', label: 'Large' }
  ]
})
Field({
  name: 'topics',
  type: 'checkbox',
  legend: 'Topics',
  options: [
    { name: 'topics', value: 'news', label: 'News' },
    { name: 'topics', value: 'events', label: 'Events' }
  ]
})
Field({ type: 'submit', value: 'Save' })

The file template and singular -error classes predate scaffold’s current upload and validation markup. Adapt them to the .field attributes and -errors elements in window when using its helpers.

Form, Newsletter, and Contact#

Form({ action = '', method = 'post', success = '', error = '', redirect = '', confirm = '', fields }, slot = []) renders a form. Pass rendered Field results through slot; the fields branch emits <field> placeholders and does not call the Field function. The data attributes do not attach submit handlers automatically.

Form({ action: '/contact', method: 'post' }, [
  Field({ type: 'email', name: 'email', label: 'Email' }),
  Field({ type: 'submit', value: 'Send' })
])

Newsletter({ heading, body, action, placeholder, submit }) renders heading text, body HTML, and a POST form. Supply placeholder to render its required email input and submit for the button text. Connect submission to your application’s JSON handler when using NodeDK server.

Contact(props = {}) is a placeholder that renders an empty form; it does not define fields or submission behavior.

Flash, Dialog, Grid, and Griditem#

  • Flash({ message = '' }): text inside .flash-message. Configure browser flash helpers with el: '.flash-message' to target it.
  • Dialog({ header }, slot = []): heading, close link, and content. The close link uses fx-back, which needs an application handler.
  • Grid({}, slot = []), Griditem({}, slot = []): wrap HTML in their matching CSS classes. Their props are unused.

Layout and Page#

Layout(props = {}, slot = []) renders a full HTML document. Properties:

  • title, description: document metadata; omitted when undefined.
  • lang: default 'en'.
  • font: stylesheet URL.
  • favicon.asset.url: PNG icon URL.
  • styles, scripts: rendered HTML tags, such as state.style() output.
  • googlekey: Maps API key; adds the script with callback=initMaps.
  • track: set false to skip calling an existing window.track().
  • host: emits window.api = window.client(host). This assumes a client factory; adapt it before using the current browser request helper.

The layout renders Flash({}), then calls existing browser initialization and tracking helpers when available. Page({ title }, slot = []) emits a <layout> placeholder around header, main, and footer regions; compose it with Layout() in your application rather than relying on tag expansion.

Search({ title, action, placeholder, suggestion }) is a search template. action enables a GET form with input s; the input needs placeholder. suggestion is HTML. The current function reads an undeclared slot; add a slot = [] parameter in the copied component before rendering result content.

Created by Vidar Eldøy