Nuro is a reactive, component-based JavaScript framework which can be used as a lighter and simpler alternative to frameworks like React or Vue.
import { Nuro } from 'nuro'
class App {
name = 'world'
template = `
<div id="app">
Name: <input $bind="name"/>
Hello, {{name}}!
</div>
`
}
Nuro.mount(App)
- Lightweight (3.5kb minified and gzipped)
- Zero dependencies
- Easy to learn and simple to use
- Can be used for your entire application or sprinkled in
- No build step required
- Class based components without importing the framework
- Powerful template syntax
- UI automatically updates when data changes
- Updates only the DOM elements that have changed
- Event handling
- Automatically encodes HTML from users to prevent script injection
Nuro can be downloaded from npm or the dist directory of this repository.
Install using npm:
npm install nuro
Import as an ES Module:
import { Nuro } from 'nuro'
Nuro.mount(MyApp)
Using Nuro doesn't require a build step so the library can also be included in a simple HTML page:
<script type="module">
import { Nuro } from 'path/to/nuro.js'
Nuro.mount(MyApp)
</script>
Or use the UMD version:
<script src="path/to/nuro.umd.js"></script>
<script>
Nuro.mount(MyApp)
</script>
Components are defined using JavaScript classes:
class MyComponent {
template = `<div id="app">Hello, world</div>`
}
Component classes don't require any base class to be extended or functions to be imported. Just add a template and any lifecycle hooks you need and pass it to Nuro.mount()
. Nuro will instantiate an instance of your component, compile the template and call any hooks automatically.
Mounting will render the initial version of your UI using the default state of the component. By default your component will be mounted on a new element which is automatically appended to the body of the page:
Nuro.mount(MyComponent)
If you want to mount on a specific element instead, you can provide the element as the second argument:
Nuro.mount(MyComponent, document.querySelector('#app'))
You can also pass props as the third argument:
Nuro.mount(MyComponent, document.querySelector('#app'), { foo: 'bar' })
Props are variables you can pass to a component to be used during rendering. They are stored in an object in your component called props
and can be referenced in your templates:
class Greeter {
template = `<p>Hello, {{props.name}}</p>`
}
Nuro.mount(Greeter, document.querySelector('#app'), { name: 'world' })
State is data that can be stored in your component and persist between renders. Getting and setting state is extremely easy. Just set a property on your component and then it can be referenced by name in your template:
class App {
msg = "My state variable"
template = `<div>{{msg}}</div>`
}
Nuro.mount(App)
Changing state or props on a component instance will automatically trigger a re-render. The mount function always returns the instance of the component that was created. You can then change any properties you want and the UI will be updated accordingly.
class Status {
text = 'Waiting...'
template = `<p>{{text}}</p>`
}
let status = Nuro.mount(Status)
setTimeout(() => status.text = 'Done!', 1000)
Component state can also be changed internally using event handlers. Just add an attribute with '@' and an event type and set the value to a function:
class Clicker {
count = 0
template = `<button @click="increment">Clicked {{count}} times</button>`
increment() {
this.count++
}
}
HTML attributes can be bound to a JavaScript variable by putting a colon in front of the attribute name. This is essentially a shorthand for having an attribute value with a curly braces expression.
class Hello {
myID = 'my-id'
custom = 'Custom attribute data...'
template = `
<div :id="myID" :data-custom="custom">
Hello
</div>
`
}
Which is equivalent to:
class Hello {
myID = 'my-id'
custom = 'Custom attribute data...'
template = `
<div id="{{myID}}" data-custom="{{custom}}">
Hello
</div>
`
}
Template directives are special HTML attributes that add dynamic behavior, such as if statements and loops.
Only renders the element if the condition is truthy
class Peekaboo {
show = false
template = `
<div>
<button @click="toggleText">{{show ? 'Hide' : 'Show'}}</button>
<p $if="show">Peekaboo!</p>
</div>
`
toggleText() {
this.show = !this.show
}
}
Renders a list of elements
class Todos {
tasks = ['First', 'Second', 'Third']
template = `
<div>
<h1>Todo List</h1>
<ul>
<li $for="task in tasks">{{task}}</li>
</ul>
</div>
`
}
Used for two-way binding of form inputs. When the UI changes the data property changes and vice versa. Works with inputs, textareas and select elements.
class UserForm {
name = ''
admin = false
template = `
<form @submit="handleSubmit">
<label>Name: </label>
<input $bind="name"><br>
<label>Is admin: </label>
<input type="checkbox" $bind="admin"><br>
<input type="submit">
</form>
`
handleSubmit(e) {
e.preventDefault()
alert(`Name: ${this.name}, Admin: ${this.admin}`)
}
}
Used to easily toggle classes on and off. The value is an object where the property names are the classes to toggle and the property values are whether the class should be rendered or not.
class ActivateButton {
selected = false
template = `
<button $class="{active: selected}" @click="()=>selected=true">
Activate
</button>
`
}
Used to merge props from an object onto an element or component
class Example {
myProps = {
id: 'my-id',
class: 'important'
}
template = `
<div $attrs="myProps">
Example
</div>
`
}
In some cases you may want to write the rendering logic yourself instead of using a template string. A render method is a function that produces the DOM structure for the component. It has one parameter which is a function that creates a single virtual DOM element. This is called the createElement function. It can be called anything in your render method but the documentation will use h
. This concept should be familiar to developers who have worked with React or similar frameworks.
The following render method:
class MyComponent {
render(h) {
return h('div', {id: 'app'}, [
h('h1', {}, ['Nuro is: ']),
h('ul', {class: 'list'}, [
h('li', {}, ['Fun']),
h('li', {}, ['Easy to learn'])
])
])
}
}
will produce this HTML:
<div id="app">
<h1>Nuro is: </h1>
<ul class="list">
<li>Fun</li>
<li>Easy to learn</li>
</ul>
</div>
You can also use state/props in your method:
class MyComponent {
items = ['Fun', 'Easy to learn']
render(h) {
return h('div', {id: 'app'}, [
h('h1', {}, [this.props.title]),
h('ul', {class: 'list'}, this.items.map(item => {
return h('li', {}, item)
}))
])
}
}
Nuro.mount(MyComponent, element, {title: 'Nuro is: '})
All templates are automatically compiled to render methods behind the scenes when a component is mounted for the first time. <div>{{foo}}</div>
--> return h('div', {}, this.foo)
Render methods are the most flexible way to implement components since you have the full power of JavaScript. If there is some logic that is awkward to represent as a template (such as dynamic tag names) you may want to use render methods instead. However, they tend to be tedious to write and hard to read for large components.
To include components inside other components, use an includes
object:
class ChildComponent {
template = `<p>Child component</p>`
}
class ParentComponent {
template = `
<div>
<p>Child content below...</p>
<child-component></child-component>
</div>
`
includes = {
'child-component': ChildComponent
}
}
Nuro.mount(ParentComponent)
Nuro is smart enough to convert PascalCase to kebab-case so you can also use the shorthand:
includes = {
ChildComponent
}
Or register a component globally so it can be included in all other components in your app:
Nuro.register('my-button', class {
template = `<button class="my-button">{{props.text}}</button>`
})
Use props to pass data from a parent component to a child component
class ChildComponent {
template = `<p>Data from parent: {{props.foo}}</p>`
}
class ParentComponent {
template = `
<div>
<p>Child content below...</p>
<child-component foo="Hello!"></child-component>
</div>
`
includes = {
'child-component': ChildComponent
}
}
Nuro.mount(ParentComponent)
A slot is a placeholder element for other DOM content that is passed in by the parent component. This allows a parent component to pass HTML template code to a child component instead of just JavaScript variables like props.
class PictureFrame {
template = `
<div class="frame">
<slot></slot>
</div>
`
}
class Gallery {
template = `
<div>
<picture-frame>
<img src="photo1.jpg"/>
</picture-frame>
<picture-frame>
<img src="photo2.jpg"/>
</picture-frame>
</div>
`
includes = {
PictureFrame
}
}
Nuro.mount(Gallery)
To implement a lifecycle hook, just define a method on the component with the correct name.
class App {
text = 'Fetching...'
template = `<div>{{text}}</div>`
async afterMount() {
let response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
let json = await response.json()
this.text = json.title
}
}
Nuro.mount(App)
Each hook is listed below:
beforeInit
- After the component instance is created but before reactivity is addedbeforeMount
- Component is fully set up but has not been mounted to the DOMbeforeRender
- Before every renderafterRender
- After every renderafterMount
- After initial render to the DOMbeforeUnmount
- Before the component is removed from the DOM
import { Nuro, Component, CreateElement } from 'nuro'
interface MyProps {
id: number
message: string
}
class MyComponent extends Component<MyProps> {
render(h: CreateElement) {
return h('p', { 'data-message-id': this.props.id }, [
this.props.message
])
}
}
Nuro.mount(MyComponent, element, {
id: 1,
message: 'Hello, world'
})
Pull requests and feedback are welcome. If you find a bug please create an issue.