Skip to content

A small Direct DOM library for creating user interfaces.

License

Notifications You must be signed in to change notification settings

TrigenSoftware/nanoviews

Repository files navigation

nanoviews

ESM-only package NPM version Dependencies status Install size Build status Coverage status

A small Direct DOM library for creating user interfaces.

  • Small. Between 2.71 and 4.77 kB (minified and brotlied). Zero dependencies*.
  • Direct DOM. Less CPU and memory usage compared to Virtual DOM.
  • Designed for best Tree-Shaking: only the code you use is included in your bundle.
  • TypeScript-first.
import { atom } from 'nanostores'
import { div, a, img, h1, button, p, render } from 'nanoviews'

function App() {
  const $counter = atom(0)

  return div()(
    a({ href: 'https://vitejs.dev', target: '_blank' })(
      img({ src: './vite.svg', class: 'logo', alt: 'Vite logo' })
    ),
    a({ href: 'https://github.com/TrigenSoftware/nanoviews', target: '_blank' })(
      img({ src: './nanoviews.svg', class: 'logo nanoviews', alt: 'Nanoviews logo' })
    ),
    h1()('Vite + Nanoviews'),
    div({ class: 'card' })(
      button({
        onClick() {
          $counter.set($counter.get() + 1)
        }
      })('count is ', $counter)
    ),
    p({ class: 'read-the-docs' })('Click on the Vite and Nanoviews logos to learn more')
  )
}

render(App(), document.querySelector('#app'))

Install   •   Reactivity   •   Basic markup   •   Special methods   •   Effect attributes   •   Logic methods

Install

pnpm add -D nanoviews
# or
npm i -D nanoviews
# or
yarn add -D nanoviews

Reactivity

Nanoviews is designed to be used with Nano Stores for reactivity. But Nano Stores is not a direct dependency of Nanoviews, so you can use any other reactive library that implements store interface.

import { atom } from 'nanostores'
import { fragment, input, p } from 'nanoviews'

const $text = atom('')

fragment(
  input({
    onInput(event) {
      $text.set(event.target.value)
    }
  }),
  p()($text)
)

Basic markup

Nanoviews provides a set of methods for creating HTML elements with the specified attributes and children. Every method creates a "block" that represents a DOM node or another block(s).

Child can be an another element, primitive value (string, number, boolean, null or undefined), store with primitive or array of children. Attributes also can be a primitive value or store.

import { atom } from 'nanostores'
import { ul, li } from 'nanoviews'

const $boolean = atom(true)

ul({ class: 'list' })(
  li()('String value'),
  li()('Number value', 42),
  li()('Boolean value', $boolean)
)

Special methods

text$

text$ is a method that creates text node block with the specified value or store.

import { atom } from 'nanostores'
import { text$, effect$ } from 'nanoviews'

function TickTak() {
  const $tick = atom(0)
  const effect = () => {
    const id = setInterval(() => {
      $tick.set($tick.get() + 1)
    }, 1000)

    return () => clearInterval(id)
  }

  return effect$(
    effect,
    text$($tick)
  )
}

fragment

fragment is a method that creates a fragment block with the specified children.

import { atom } from 'nanostores'
import { fragment, effect$ } from 'nanoviews'

function TickTak() {
  const $tick = atom(0)
  const effect = () => {
    const id = setInterval(() => {
      $tick.set($tick.get() + 1)
    }, 1000)

    return () => clearInterval(id)
  }

  return effect$(
    effect,
    fragment('Tick tak: ', $tick)
  )
}

dangerouslySetInnerHTML

dangerouslySetInnerHTML is a method that sets the inner HTML of the element block. It is used for inserting HTML from a source that may not be trusted.

import { div, dangerouslySetInnerHTML } from 'nanoviews'

dangerouslySetInnerHTML(
  div({ id: 'rendered-md' }),
  '<p>Some text</p>'
)

attachShadow

attachShadow is a method that attaches a shadow DOM to the specified element block.

import { div, attachShadow } from 'nanoviews'

attachShadow(
  div({ id: 'custom-element' }),
  {
    mode: 'open'
  }
)(
  'Nanoviews can shadow DOM!'
)

Effect attributes

Effect attributes are special attributes that can control element's behavior.

ref$

ref$ is an effect attribute that can provide a reference to the DOM node.

import { atom } from 'nanostores'
import { div, ref$ } from 'nanoviews'

const $ref = atom(null)

div({
  [ref$]: $ref
})(
  'Target element'
)

style$

style$ is an effect attribute that manages the style of the element.

import { atom } from 'nanostores'
import { button, style$ } from 'nanoviews'

const $color = atom('white')

button({
  [style$]: {
    color: $color,
    backgroundColor: 'black'
  }
})(
  'Click me'
)

classList$

classList$ is an effect attribute that manages class names of the element.

import { button, classList$, classIf$, classGet$ } from 'nanoviews'
import * as styles from './styles.css'

function MyButton({
  class: className,
  theme = 'primary',
  rounded = false
}) {
  return button({
    [classList$]: [className, 'myButton', classIf$(styles.rounded, rounded), classGet$(styles, theme)]
  })
}

autoFocus$

autoFocus$ is an effect attribute that sets the auto focus on the element.

import { input, autoFocus$ } from 'nanoviews'

input({
  type: 'text',
  [autoFocus$]: true
})

value$

value$ is an effect attribute that manages the value of text inputs.

import { atom } from 'nanostores'
import { textarea, value$ } from 'nanoviews'

const $review = atom('')

textarea({
  name: 'review',
  [value$]: $review
})(
  'Write your review here'
)

checked$

checked$ is an effect attribute that manages the checked state of checkboxes and radio buttons.

import { atom } from 'nanostores'
import { input, checked$, Indeterminate } from 'nanoviews'

const $checked = atom(false)

input({
  type: 'checkbox',
  [checked$]: $checked
})

Also you can manage indeterminate state of checkboxes:

$checked.set(Indeterminate)

selected$

selected$ is an effect attribute that manages the selected state of select's options.

import { atom } from 'nanostores'
import { select, option, selected$ } from 'nanoviews'

const $selected = atom('mid')

select({
  name: 'player-pos',
  [selected$]: $selected
})(
  option({
    value: 'carry'
  })('Yatoro'),
  option({
    value: 'mid',
  })('Larl'),
  option({
    value: 'offlane'
  })('Collapse'),
  option({
    value: 'support'
  })('Mira'),
  option({
    value: 'full-support',
  })('Miposhka'),
)

Multiple select:

const $selected = atom(['mid', 'carry'])

select({
  name: 'player-pos',
  [selected$]: $selected
})(
  option({
    value: 'carry'
  })('Yatoro'),
  option({
    value: 'mid',
  })('Larl'),
  option({
    value: 'offlane'
  })('Collapse'),
  option({
    value: 'support'
  })('Mira'),
  option({
    value: 'full-support',
  })('Miposhka'),
)

files$

files$ is an effect attribute that can provide the files of file inputs.

import { atom } from 'nanostores'
import { input, files$ } from 'nanoviews'

const $files = atom([])

input({
  type: 'file',
  [files$]: $files
})

Logic methods

effect$

effect$ is a method that add effects to the element block.

import { div, effect$ } from 'nanoviews'

function App() {
  const effect = (element) => {
    console.log('Mounted', element.outerHTML) // Mounted <div>Hello, Nanoviews!</div>

    return () => {
      console.log('Unmounted')
    }
  }

  return effect$(
    effect, // or array of effects
    div()('Hello, Nanoviews!')
  )
}

decide$

decide$ is a method that can switch between different childs.

import { atom } from 'nanostores'
import { decide$, div, p } from 'nanoviews'

const $show = atom(false)

decide$($show, (show) => {
  if (show) {
    return div()('Hello, Nanoviews!')
  }
})

const $toggle = atom(false)

decide$($toggle, toggle => (
  toggle ? p()('Toggle is true') : div()('Toggle is false')
))

for$

for$ is a method that can iterate over an array to render a list of blocks.

import { atom } from 'nanostores'
import { for$, ul, li } from 'nanoviews'

const $players = atom([
  { id: 0, name: 'chopper' },
  { id: 1, name: 'magixx' },
  { id: 2, name: 'zont1x' },
  { id: 3, name: 'donk' },
  { id: 4, name: 'sh1ro' },
  { id: 5, name: 'hally' }
])

ul()(
  for$($players, (player) => player.id, (player) => li()(player))
)

children$

children$ is a method that creates block with optional children receiver.

import { div, children$ } from 'nanoviews'

function MyComponent(props) {
  return children$((children) => div(props)(
    'My component children: ',
    children || 'empty'
  ))
}

MyComponent() // <div>My component children: empty</div>

MyComponent()('Hello, Nanoviews!') // <div>My component children: Hello, Nanoviews!</div>

slots$

slots$ is a method that should be used with children$ to receive slots.

import { main, header, footer, children$, slots$, createSlot } from 'nanoviews'

const LayoutHeaderSlot = createSlot()
const LayoutFooterSlot = createSlot()

function Layout() {
  return children$(
    slots$(
      [LayoutHeaderSlot, LayoutFooterSlot],
      (headerSlot, footerSlot, children) => main()(
        header()(headerSlot),
        children,
        footer()(footerSlot)
      )
    )
  )
}

Layout()(
  LayoutHeaderSlot('Header content'),
  LayoutFooterSlot('Footer content'),
  'Main content'
)

Slot's content can be anything, including functions, that can be used to render lists:

import { ul, li, b, children$, slots$ createSlot, for$ } from 'nanoviews'

const ListItemSlot = createSlot()

function List(items) {
  return children$(
    slots$(
      [ListItemSlot],
      (listItemSlot) => ul()(
        for$(items, (item) => item.id, (item) => li()(
          listItemSlot(item.name)
        ))
      )
    )
  )
}

List([
  { id: 0, name: 'chopper' },
  { id: 1, name: 'magixx' },
  { id: 2, name: 'zont1x' },
  { id: 3, name: 'donk' },
  { id: 4, name: 'sh1ro' },
  { id: 5, name: 'hally' }
])(
  ListItemSlot((name) => b()('Player: ', name))
)

context$

context$ is a method that can provide a context to the children.

import { atom } from 'nanostores'
import { div, context$, createContext } from 'nanoviews'

const [ThemeContext, getTheme] = createContext(atom('light')) // default value

function MyComponent() {
  const theme = getTheme()

  return div()(
    'Current theme: ',
    theme
  )
}

function App() {
  const $theme = atom('dark')

  return context$(
    ThemeContext($theme),
    () => MyComponent()
  )
}

App() // <div>Current theme: dark</div>

portal$

portal$ is a method that can render a block in a different place in the DOM.

import { div, portal$ } from 'nanoviews'

portal$(
  document.body,
  div()('I am in the body!')
)

await$

await$ is a method that can handle and render promises.

import { atom } from 'nanostores'
import { i, b, await$, pending$, then$, catch$ } from 'nanoviews'

const $promise = atom(Promise.resolve('Hello, Nanoviews!'))

await$($promise)(
  pending$(() => i()('Loading...')),
  then$((value) => b()(value)),
  catch$((error) => String(error))
)

forAwait$

forAwait$ is a method that can handle and render async iterables.

import { ul, li, forAwait$, pending$, each$, then$, catch$ } from 'nanoviews'

ul()(
  forAwait$(fetchProducts())(
    pending$(() => li()('Loading...')),
    each$((product) => ProductView(product)),
    then$((count) => li()('Loaded products count: ', count)),
    catch$((error) => li()('Error: ', error))
  )
)

Also you can render list in reversed order:

import { main, div, forAwait$, pending$, each$, then$, catch$ } from 'nanoviews'

main()(
  forAwait$(fetchNewsFeed(), true)(
    pending$(() => div()('Loading...')),
    each$((product) => PostView(product)),
    then$((count) => div()('Loaded products count: ', count)),
    catch$((error) => div()('Error: ', error))
  )
)

throw$

throw$ is a helper to throw an error in expressions.

import { ul, children$, throw$ } from 'nanoviews'

function MyComponent() {
  return children$((children = throw$(new Error('Children are required'))) => ul()(children))
}

About

A small Direct DOM library for creating user interfaces.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project