nanoviews

1.0.0-alpha.2 • Public • Published

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 3.8 and 6.4 kB (minified and brotlied). Zero external 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 { signal } from 'nanoviews/store'
import { div, a, img, h1, button, p, mount } from 'nanoviews'

function App() {
  const $counter = signal(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($counter() + 1)
        }
      })('count is ', $counter)
    ),
    p({ class: 'read-the-docs' })('Click on the Vite and Nanoviews logos to learn more')
  )
}

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

Install   •   Reactivity   •   Basic markup   •   Effect attributes   •   Components   •   Logic methods   •   Special methods   •   Why?

Install

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

Reactivity

Nanoviews is using Kida under the hood for reactivity. Kida is a signal library inspired by Nano Stores and was build specially for Nanoviews.

import { signal } from 'nanoviews/store' // or import { signal } from 'kida'
import { fragment, input, p } from 'nanoviews'

const $text = signal('')

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

Basicly, under the hood, reactivity works something like that:

import { signal, effect } from 'kida'

const $text = signal('')
const textNode = document.createTextNode('')

effect(() => {
  textNode.data = $text()
})

Basic markup

Nanoviews provides a set of methods for creating HTML elements with the specified attributes and children. Every method creates a DOM node.

Child can be an another DOM node, primitive value (string, number, boolean, null or undefined) or signal with primitive. Attributes also can be a primitive value or signal.

import { signal } from 'nanoviews/store'
import { ul, li } from 'nanoviews'

const $boolean = signal(true)
const list = ul({ class: 'list' })(
  li()('String value'),
  li()('Number value', 42),
  li()('Boolean value', $boolean)
)
// `list` is HTMLUListElement instance

mount

mount is a method that mounts the component to the specified container.

import { signal } from 'nanoviews/store'
import { div, h1, p, mount } from 'nanoviews'

function App() {
  return div()(
    h1()('Nanoviews App'),
    p()('Hello World!')
  )
}

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

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 { signal } from 'nanoviews/store'
import { div, ref$ } from 'nanoviews'

const $ref = signal(null)

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

style$

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

import { signal } from 'nanoviews/store'
import { button, style$ } from 'nanoviews'

const $color = signal('white')

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

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 { signal } from 'nanoviews/store'
import { textarea, value$ } from 'nanoviews'

const $review = signal('')

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 { signal } from 'nanoviews/store'
import { input, checked$, Indeterminate } from 'nanoviews'

const $checked = signal(false)

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

Also you can manage indeterminate state of checkboxes:

$checked(Indeterminate)

selected$

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

import { signal } from 'nanoviews/store'
import { select, option, selected$ } from 'nanoviews'

const $selected = signal('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 = signal(['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 { signal } from 'nanoviews/store'
import { input, files$ } from 'nanoviews'

const $files = signal([])

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

Components

Components are the building blocks of any application. These units are reusable and can be combined to create more complex applications.

Components are functions that return primitive or DOM node:

function MyComponent() {
  return div()('Hello, Nanoviews!')
}

effect

effect is a method that add effects to the component.

import { div, effect } from 'nanoviews'

function MyComponent() {
  effect(() => {
    console.log('Mounted')

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

  return div()('Hello, Nanoviews!')
}

Also you can use effect with signals:

import { div, effect } from 'nanoviews'

const $timeout = signal(1000)

function MyComponent() {
  let intervalId

  effect(() => {
    intervalId = setInterval(() => {
      console.log('Tick')
    }, $timeout())

    return () => {
      clearInterval(intervalId)
    }
  })

  return div()('Hello, Nanoviews!')
}

children$

children$ is a method that creates 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 to receive slots and rest children.

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

function LayoutHeader(text) {
  return slot$(LayoutHeader, text)
}

function LayoutFooter(text) {
  return slot$(LayoutFooter, text)
}

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

Layout()(
  LayoutHeader('Header content'),
  LayoutFooter('Footer content'),
  'Main content'
)

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

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

function ListItem(renderItem) {
  return slot$(ListItem, renderItem)
}

function List(items) {
  return slots$(
    [ListItem],
    (listItemSlot) => ul()(
      for$(items)(
        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' }
])(
  ListItem(name => b()('Player: ', name))
)

context$

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

import { signal } from 'nanoviews/store'
import { div, context$, provide, inject } from 'nanoviews'

function ThemeContext() {
  return signal('light') // default value
}

function MyComponent() {
  const $theme = inject(ThemeContext)

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

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

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

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

[!NOTE] Nanoviews contexts are based on Kida's dependency injection system.

Logic methods

if$

if$ is a method that can render different childs based on the condition.

import { signal } from 'nanoviews/store'
import { if$, div, p } from 'nanoviews'

const $show = signal(false)

if$($show)(
  () => div()('Hello, Nanoviews!')
)

const $toggle = signal(false)

if$($toggle)(
  () => p()('Toggle is true'),
  () => div()('Toggle is false')
)

switch$

switch$ is a method like if$ but with multiple conditions.

import { signal } from 'nanoviews/store'
import { switch$, case$, default$, div, p } from 'nanoviews'

const $state = signal('loading')

switch$(state)(
  case$('loading', () => b()('Loading')),
  case$('error', () => b()('Error')),
  default$(() => 'Success')
)

for$

for$ is a method that can iterate over an array to render a fragment of elements.

import { signal, record } from 'nanoviews/store'
import { for$, trackById, ul, li } from 'nanoviews'

const $players = signal([
  { 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, trackById)(
    $player => li()(
      record($player).$name
    )
  )
)

There are exported predefined trackById function to track by id property and trackBy(key) function to create a tracker for specified key.

portal$

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

import { div, portal$ } from 'nanoviews'

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

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))
}

Special methods

fragment

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

import { signal } from 'nanoviews/store'
import { fragment, effect } from 'nanoviews'

function TickTak() {
  const $tick = signal(0)

  effect(() => {
    const id = setInterval(() => {
      $tick($tick() + 1)
    }, 1000)

    return () => clearInterval(id)
  })

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

dangerouslySetInnerHtml

dangerouslySetInnerHtml is a method that sets the inner HTML of the element. 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>'
)

shadow

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

import { div, shadow } from 'nanoviews'

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

Why?

Bundle size

Nanoviews and Kida are small libraries and designed to be tree-shakable. So apps using Nanoviews and Kida can be smaller even than using SolidJS or Svelte!

Example Nanoviews SolidJS Svelte
Vite Demo 7.66 kB / gzip: 3.22 kB
source code
8.93 kB / gzip: 3.73 kB
source code
12.73 kB / gzip: 5.54 kB kB
source code
Weather + Kida
17.27 kB / gzip: 6.96 kB
source code
+ nanostores + @nanostores/solid
25.23 kB / gzip: 9.80 kB
source code
+ nanostores
26.80 kB / gzip: 10.84 kB
source code

Performance

Nanoviews is not fastest library: SolidJS and Svelte are faster, but performance is close to them. Anyway, Nanoviews is faster than React 🙂.

krausest js-framework-benchmark

Readme

Keywords

Package Sidebar

Install

npm i nanoviews

Weekly Downloads

8

Version

1.0.0-alpha.2

License

MIT

Unpacked Size

348 kB

Total Files

109

Last publish

Collaborators

  • dangreen