Reference

Reactive components

Reactive components are interactive and make your pages feel dynamic. They respond to the user's mouse and keyboard events, and enhance your content-focused pages with small interactive "islands". They are also the building blocks for the more complex single-page applications.

Example

Let's start with an example. Here is a classic image gallery component that you can interact with the arrow buttons as well as with the small round dots below the image.

Here's the code for our gallery component:

<section ="image-gallery" class="gallery" translate="no">

  <div>
    <a class="seek prev" ="index--" :if="index"></a>

    <img src="{ basedir }/{ images[index] }">

    <a class="seek next" ="index++"
      :if="images.length - index > 1"></a>
  </div>

  <nav>
    <a :for="src, i in images"
      class="{ current: i == index }"
      ="index = i"></a>
  </nav>

  <script>
    index = 0
  </script>

</section>

You can see some basic reactivity in there like loops, and conditionals that respond to user events.

Event handlers

Attributes starting with the @ symbol define an event handler. These are JavaScript functions that are called on user interaction like click, keypress, or mouse move.

Inline handlers

Inline handlers are defined directly on the attribute:

<button ="count++">Increment</button>

Inline handlers are good for simple expressions.

Method handlers

More complex functionality should go to an instance method

<dialog>
  <button ="close">Close</button>

  <script>
    close() {
      this.root.close()
      location.hash = ''
    }
  </script>
</dialog>

Method calls

You can pass arguments for the method calls:

<div>
  <button ="say('yo!')">Say yo!</button>

  <script>
    say(msg) {
      console.log(msg)
    }
  </script>
</div>

Event argument

The method handler always receives Event object as the last argument, unless it is explicitly given on the method call with a name $event:

<div>
  <button ="first">First</button>
  <button ="second('Hello')">World</button>
  <button ="third('Hello', $event, 'World')">Nue</button>

  <script>
    // prints "First"
    first($event) {
      console.info($event.target.textContent)
    }

    // prints "Hello World"
    second(hey, $event) {
      console.info(hey, $event.target.textContent)
    }

    // prints "Hello Nue World"
    third(hey, $event, who) {
      console.info(hey, $event.target.textContent, who)
    }
  </script>
</div>

Event modifiers

Nue provides some handy shortcuts to deal with the common DOM event manipulation functions. For example, @submit.prevent is a shortcut to event.preventDefault().

<!-- prevent the default event from occurring-->
<form .prevent="onSubmit"></form>

<!-- modifiers can be chained -->
<a .stop.prevent="doThat"></a>

<!-- run the modifier only -->
<form .prevent></form>

The following modifiers are supported:

Key modifiers

Key modifier binds the event handler to a specific keyboard key:

<!-- only call `submit` when the `key` is `Enter` -->
<input .enter="submit">

You can directly use any valid key names exposed via KeyboardEvent.key as modifiers by converting them to kebab-case. So the following handler will only be called if event.key is equal to 'PageDown'.

<input .page-down="onPageDown">

Nue provides the following aliases for the most commonly used keys:

Reactive loops

When you define a for loop, with the :for- expression Nue detects if the looped array is mutated and triggers necessary UI updates. These array methods include:

Replacing the array

Mutation methods mutate the original array they are called on. The non-mutating methods like filter(), concat() and slice() always return a new array in which case you should replace the old array with the new one and Nue will render accordingly:

search() {
  this.items = this.items.filter(item => item.text.match(/Foo/))
}

Loop animations

Nue lets you define an oninsert callback function that is called every time a new item is added to any of the array properties in the component. This gives you the possibility to add a CSS transition effect (among other things) for the newly added dom nodes. For example:

Here's the source code for the above demo:

<article ="animation-demo" class="user-list">

  <button ="addUser" :disabled="users[5]">
    Add user
  </button>

  <section class="user-list" translate="no">
    <media-object :for="user in users" :bind="user"/>
  </section>

  <script>

    // fill list with the first three available items
    constructor({ items }) {
      this.users = items.slice(0, 3)
    }

    // insert a new user from the available items
    addUser() {
      const { items, users } = this
      const user = items[users.length]
      if (user) users.push(user)
    }

    // add a CSS transition class to a newly added dom nodes
    oninsert(node) {
|     setTimeout(() => node.classList.add('fade-in'), 1)
    }
  </script>

</article>

Miscellaneous concepts

Lifecycle methods

Each component instance goes through a series of steps during its lifetime: first, it is created, then mounted on the page, and then it gets updated one or more times. Sometimes the component is removed or "unmounted" from the page.

You can hook custom functionality to these steps by creating instance methods with a specific name:

<script>

  // when the component is created. data/args is given as the first argument
  constructor(data) {

  }

  // after the component is mounted on the page
  mounted(data) {

  }

  // after the component is updated
  updated() {

  }

  // after the component is removed from the page
  unmounted() {

  }
</script>

Inside the callback function this points to instance API.

Instance API

The application instance is accessible as a return value to createApp and via this variable inside the lifecycle methods. It has the following attributes and methods:

$el the root DOM node of the component instance

$parent is the root DOM node of the parent instance

$refs access to named DOM nodes and inner components inside the component

mount(root: DOMElement) mount the instance to the given root element

unmount() method to remove the component from the current component tree

update(data?: Object) forces the component instance to re-render with optional data. You typically call this event after fetching data from the server or some other asynchronous event.

mountChild(name, wrap, data) mounts a new child component on a DOM element inside the current component.

The component re-renders itself automatically after calling an event handler, but you need to call this manually if there is no clear interaction to be detected.

References to nodes and components

Sometimes you want to get handle to some DOM element or nested component inside your root component. Those are available via the $refs property on the application instance:

<div ="my-component">

  <!-- name a DOM node with "ref" attribute -->
  <figure ref="image"></figure>

  <!-- or with "name" attribute -->
  <input name="email" placeholder="Hey, dude">

  <!-- custom elements are automatically named -->
  <image-gallery/>

  <!-- refs work on templates too-->
  <h3>{ $refs.email.placeholder }</h3>

  <script>

    // references are available after mount
    mounted() {

      // get a handle to the image DOM node
      const image = this.$refs.image

      // get a handle to image-gallery component API
      const gallery = this.$refs['image-gallery']
    }
  </script>
</div>

Sharing code between components

You can add and import shared code inside a top-level <script> tag. Here's an example library that defines both a shopping cart and a button component that adds items to the cart. The cart itself is defined in "cart.js", which is a plain JavaScript file. This cart is used by both components.

<!-- shared code -->
<script>
|  import { shopping_cart, addToCart } from './cart.js'
</script>

<!-- shopping bag component -->
<article ="shopping-cart">
  <div :for="item in items">
    <h3>{ item.price }</h3>
    <p>{ item.amount }</p>
  </div>

  <script>
    constructor() {
     this.items = shopping_cart.getItems()
    }
  </script>
</article>

<!-- "add to cart" component -->
<button ="add-to-cart" ="click">
  <script>
    click() {
     addToCart(this.data)
    }
  </script>
</button>

Mounting reactive components

Nue components are usually auto-mounted by the framework, but you can also create applications manually on a JavaScript or TypeScript module as follows:

import createApp from '/@nue/nue.js'
import { default as MyApp, lib } from './myapp.js'

// create app
const app = createApp(MyApp, { value: 10 }, lib)

// get some wrapper element
const wrapper = document.querySelector('#my-app')

// mount the app
app.mount(wrapper)

The createApp() method

  1. component the root component to be mounted
  2. data optional data or data model for the component. The data can contain any number of properties and methods
  3. deps optional array of nested/dependant components in a more complex app.

Mounting more complex apps

You can import any number of components and pass them all as dependencies for the creatApp method:

// import createApp method
import createApp from '/@nue/nue.js'

// import compiled Nue library files
import { lib as lib1 } from './lib1.js'
import { lib as lib2 } from './lib2.js'
import { lib as lib3 } from './lib3.js'

// import compiled Nue App
import App from './app.js'

// import some data for the app
import model from './model.js'

// create the app with the dependencies
const app = createApp(App, model, [...lib1, ...lib2, ...lib3])