├── .gitignore ├── src ├── index-umd.ts ├── util │ ├── nuro-error.ts │ ├── string-utils.ts │ └── object-utils.ts ├── api │ ├── plugin.ts │ ├── create-element.ts │ ├── vnode.ts │ ├── component-proxy.ts │ ├── component.ts │ └── global-api.ts ├── components │ ├── hooks.ts │ ├── register.ts │ ├── plugins.ts │ ├── mixins.ts │ ├── create-element.ts │ ├── proxy-handler.ts │ ├── component-handler.ts │ └── template-compiler.ts ├── dom │ ├── html-to-dom.ts │ ├── map-vnode.ts │ ├── node-context.ts │ ├── dom-patcher.ts │ └── diff-engine.ts └── index.ts ├── tsconfig.json ├── __tests__ ├── compiler │ ├── slots.test.js │ ├── if-directive.test.js │ ├── attribute-binding.test.js │ ├── includes.test.js │ ├── event-handlers.test.js │ ├── attrs-directive.test.js │ ├── comments.test.js │ ├── for-directive.test.js │ ├── bind-directive.test.js │ ├── basics.test.js │ ├── expressions.test.js │ ├── class-directive.test.js │ └── integration.test.js ├── integration │ ├── security.test.js │ ├── plugins.test.js │ ├── mixins.test.js │ ├── unmount.test.js │ ├── state.test.js │ ├── events.test.js │ ├── nested-components.test.js │ ├── props.test.js │ ├── mount.test.js │ ├── update-arrays.test.js │ ├── hooks.test.js │ ├── render.test.js │ └── update.test.js └── unit │ ├── string-utils.test.js │ ├── object-utils.test.js │ └── create-element.test.js ├── rollup.config.js ├── LICENSE ├── package.json ├── dist ├── nuro.min.js ├── nuro.umd.min.js ├── nuro.js └── nuro.umd.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | compiled 4 | -------------------------------------------------------------------------------- /src/index-umd.ts: -------------------------------------------------------------------------------- 1 | import { Nuro } from './index' 2 | 3 | export default Nuro 4 | -------------------------------------------------------------------------------- /src/util/nuro-error.ts: -------------------------------------------------------------------------------- 1 | export class NuroError extends Error { 2 | constructor(message: string) { 3 | super(message) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/api/plugin.ts: -------------------------------------------------------------------------------- 1 | import { GlobalAPI } from './global-api.js' 2 | 3 | export interface Plugin { 4 | install: (globalAPI: GlobalAPI, options?: any) => void 5 | } 6 | -------------------------------------------------------------------------------- /src/components/hooks.ts: -------------------------------------------------------------------------------- 1 | import { ComponentProxy } from '../api/component-proxy.js' 2 | import { Component } from '../api/component.js' 3 | 4 | export function callHook(component: Component | ComponentProxy, hookName: string) { 5 | if (component[hookName]) { 6 | component[hookName]() 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "ES6", 5 | "outDir": "compiled", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "declaration": true 11 | } 12 | } -------------------------------------------------------------------------------- /src/api/create-element.ts: -------------------------------------------------------------------------------- 1 | import { Component } from './component.js' 2 | import { VNode } from './vnode.js' 3 | 4 | export interface CreateElement { 5 | ( 6 | type: string | (new (props: any) => Component), 7 | props?: Record, 8 | children?: (VNode | string)[] | VNode | string 9 | ): VNode 10 | } 11 | -------------------------------------------------------------------------------- /src/dom/html-to-dom.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert a HTML string representing one DOM node and turns 3 | * it into a real DOM node. 4 | */ 5 | export function htmlToDom(html: string): Element { 6 | let document = new DOMParser().parseFromString(html, 'text/html') 7 | let wrapperNode = document.body 8 | return wrapperNode.children[0] 9 | } 10 | -------------------------------------------------------------------------------- /__tests__/compiler/slots.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | let compileTemplate = Nuro.compileTemplate 3 | 4 | test('should compile slot', () => { 5 | let code = compileTemplate('

Hello

') 6 | expect(code) 7 | .toEqual("with(this){return h('div',{},[h('h1',{},['Hello']),...props.children])}") 8 | }) -------------------------------------------------------------------------------- /src/api/vnode.ts: -------------------------------------------------------------------------------- 1 | import { ComponentProxy } from './component-proxy.js' 2 | import { ComponentClass } from './component.js' 3 | 4 | export interface VNode { 5 | nodeType: 'component' | 'element' | 'text' | 'comment' 6 | tag: string 7 | text: string 8 | attrs: Record 9 | children: VNode[] 10 | componentClass?: ComponentClass 11 | } 12 | -------------------------------------------------------------------------------- /src/components/register.ts: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from '../api/component.js' 2 | import { camelCaseToKebabCase } from '../util/string-utils.js' 3 | 4 | export const globalIncludes = new Map() 5 | 6 | export function register(componentName: string, ComponentClass: ComponentClass): void { 7 | globalIncludes.set(camelCaseToKebabCase(componentName), ComponentClass) 8 | } 9 | -------------------------------------------------------------------------------- /src/components/plugins.ts: -------------------------------------------------------------------------------- 1 | import { GlobalAPI } from '../api/global-api.js' 2 | import { Plugin } from '../api/plugin.js' 3 | 4 | const installedPlugins: Array = [] 5 | 6 | export function installPlugin(this: GlobalAPI, plugin: Plugin, options?: any): void { 7 | if (!installedPlugins.includes(plugin)) { 8 | plugin.install(this, options) 9 | installedPlugins.push(plugin) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/api/component-proxy.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentClass, Render } from './component.js' 2 | import { VNode } from './vnode.js' 3 | 4 | export interface InjectedProps { 5 | props: Record 6 | $element: Element 7 | $vnode: VNode 8 | includes: Map 9 | render: Render 10 | $update: (newData?: Record) => void 11 | } 12 | 13 | export type ComponentProxy = InjectedProps & Component 14 | -------------------------------------------------------------------------------- /__tests__/integration/security.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | 3 | test('test safe HTML encoding', () => { 4 | 5 | class TestComponent { 6 | html = '

foo

' 7 | render($) { 8 | return $('div', {id: 'app'}, this.html) 9 | } 10 | } 11 | Nuro.mount(TestComponent) 12 | 13 | expect(document.getElementById('app').outerHTML) 14 | .toEqual(`
<p id=\"unsafe\">foo</p>
`) 15 | }) -------------------------------------------------------------------------------- /__tests__/compiler/if-directive.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | let compileTemplate = Nuro.compileTemplate 3 | 4 | test('should compile $if directive', () => { 5 | let code = compileTemplate('

Show

') 6 | expect(code) 7 | .toEqual("with(this){return h('div',{},[(show)?h('p',{},['Show']):''])}") 8 | }) 9 | 10 | test('should compile $if directive with expression', () => { 11 | let code = compileTemplate('

Show

') 12 | expect(code) 13 | .toEqual("with(this){return h('div',{},[(name !== null)?h('p',{},['Show']):''])}") 14 | }) -------------------------------------------------------------------------------- /__tests__/compiler/attribute-binding.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | let compileTemplate = Nuro.compileTemplate 3 | 4 | test('should bind class attribute', () => { 5 | let code = compileTemplate('
Hello
') 6 | expect(code) 7 | .toEqual("with(this){return h('div',{'class':foo},['Hello'])}") 8 | }) 9 | 10 | test('should bind several attributes', () => { 11 | let code = compileTemplate('
Hello
') 12 | expect(code) 13 | .toEqual("with(this){return h('div',{'id':id,'class':class,'data-foo':foo},['Hello'])}") 14 | }) 15 | -------------------------------------------------------------------------------- /__tests__/compiler/includes.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | let compileTemplate = Nuro.compileTemplate 3 | 4 | test('should compile include', () => { 5 | let code = compileTemplate('
') 6 | expect(code) 7 | .toEqual("with(this){return h('div',{},[h('my-component',{})])}") 8 | }) 9 | 10 | test('should compile include with attributes and children', () => { 11 | let code = compileTemplate('
Hello
') 12 | expect(code) 13 | .toEqual("with(this){return h('div',{},[h('my-component',{'foo':'foo val'},[h('div',{},['Hello'])])])}") 14 | }) 15 | -------------------------------------------------------------------------------- /src/components/mixins.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../api/component.js' 2 | import { ComponentProxy } from '../api/component-proxy.js' 3 | 4 | const mixins: Array> = [] 5 | 6 | export function addMixin(mixin: Record): void { 7 | mixins.push(mixin) 8 | } 9 | 10 | /* 11 | Add all properties from the mixin object to the component. 12 | // TODO: if prop is a lifecycle hook, use both instead of replacing the component's 13 | */ 14 | export function applyMixins(component: Component | ComponentProxy): void { 15 | mixins.forEach(mixin => { 16 | Object.keys(mixin).forEach(prop => { 17 | component[prop] = mixin[prop] 18 | }) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /__tests__/integration/plugins.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | 3 | test('install plugin', () => { 4 | 5 | let installed = 0 6 | let newApiCalled = false 7 | let option = null 8 | 9 | let myPlugin = { 10 | install(Nuro, options) { 11 | installed++ 12 | Nuro.newApi = () => newApiCalled = true 13 | option = options.foo 14 | } 15 | } 16 | 17 | Nuro.install(myPlugin, { foo: 'foo value' }) 18 | expect(installed).toBe(1) 19 | 20 | Nuro.newApi() 21 | expect(newApiCalled).toBe(true) 22 | expect(option).toBe('foo value') 23 | 24 | // Try to install twice, should be ignored the second time 25 | Nuro.install(myPlugin, { foo: 'foo value' }) 26 | expect(installed).toBe(1) 27 | 28 | 29 | }) -------------------------------------------------------------------------------- /__tests__/integration/mixins.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | 3 | test('add mixin', () => { 4 | 5 | let hookCalled = false 6 | let dataFromComponent = '' 7 | let newMethodCalled = false 8 | 9 | Nuro.mixin({ 10 | 11 | /// Add lifecycle hook 12 | afterMount() { 13 | hookCalled = true 14 | dataFromComponent = this.foo 15 | }, 16 | 17 | // Add new method 18 | $foo() { 19 | newMethodCalled = true 20 | } 21 | 22 | }) 23 | 24 | let component = Nuro.mount(class { 25 | foo = 'foo value' 26 | template = '
' 27 | }) 28 | expect(hookCalled).toBe(true) 29 | expect(dataFromComponent).toBe('foo value') 30 | 31 | component.$foo() 32 | expect(newMethodCalled).toBe(true) 33 | 34 | }) -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | 2 | import { terser } from 'rollup-plugin-terser' 3 | 4 | export default [ 5 | { 6 | input: 'compiled/index.js', 7 | output: { 8 | file: 'dist/nuro.js', 9 | format: 'es' 10 | } 11 | }, 12 | { 13 | input: 'compiled/index.js', 14 | output: { 15 | file: 'dist/nuro.min.js', 16 | format: 'es' 17 | }, 18 | plugins: [terser()] 19 | }, 20 | { 21 | input: 'compiled/index-umd.js', 22 | output: { 23 | file: 'dist/nuro.umd.js', 24 | format: 'umd', 25 | name: 'Nuro' 26 | } 27 | }, 28 | { 29 | input: 'compiled/index-umd.js', 30 | output: { 31 | file: 'dist/nuro.umd.min.js', 32 | format: 'umd', 33 | name: 'Nuro' 34 | }, 35 | plugins: [terser()] 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /__tests__/integration/unmount.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | 3 | test('unmount', () => { 4 | document.body.innerHTML = '
' 5 | 6 | class TestComponent { 7 | foo = 'foo value' 8 | bar = 'bar value' 9 | render($) { 10 | return $('div', {id: 'app', 'data-foo': this.foo}, [ 11 | this.bar 12 | ]) 13 | } 14 | } 15 | Nuro.mount(TestComponent, document.querySelector('#app')) 16 | 17 | let result = Nuro.unmount(document.querySelector('#app')) 18 | 19 | expect(result).toBe(true) 20 | }) 21 | 22 | test('unmount element on element that doesn\'t have a component', () => { 23 | document.body.innerHTML = '
' 24 | 25 | let result = Nuro.unmount(document.querySelector('#app')) 26 | 27 | expect(result).toBe(false) 28 | }) -------------------------------------------------------------------------------- /__tests__/compiler/event-handlers.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | let compileTemplate = Nuro.compileTemplate 3 | 4 | test('should bind method to event', () => { 5 | let code = compileTemplate('
Hello
') 6 | expect(code) 7 | .toEqual("with(this){return h('div',{'@click':handleClick},['Hello'])}") 8 | }) 9 | 10 | test('should bind anonymous function to event', () => { 11 | let code = compileTemplate(`
Hello
`) 12 | expect(code) 13 | .toEqual("with(this){return h('div',{'@click':()=>alert('Hello')},['Hello'])}") 14 | }) 15 | 16 | test('should bind multi-statement anonymous function to event', () => { 17 | let code = compileTemplate(`
Hello
`) 18 | expect(code) 19 | .toEqual("with(this){return h('div',{'@click':()=>{alert('Hello');alert('World');}},['Hello'])}") 20 | }) -------------------------------------------------------------------------------- /src/util/string-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a camel case string to kebab case: 3 | * myStringValue -> my-string-value 4 | */ 5 | export function camelCaseToKebabCase(camelCase: string): string { 6 | // Map each character 7 | return camelCase 8 | .split('') 9 | .map((char, i) => { 10 | // If char is an upper case letter 11 | if (isLetter(char) && char === char.toUpperCase()) { 12 | // Make it lower case and also add a hyphen if it is not the first char 13 | if (i === 0) { 14 | return char.toLowerCase() 15 | } else { 16 | return '-' + char.toLowerCase() 17 | } 18 | } else { 19 | // Else return the lower case char 20 | return char 21 | } 22 | }) 23 | .join('') 24 | } 25 | 26 | export function isLetter(character: string) { 27 | return character.length === 1 && character.toLowerCase() != character.toUpperCase() 28 | } 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { mountRootComponent, unmountComponent } from './components/component-handler.js' 2 | import { compileTemplate } from './components/template-compiler.js' 3 | import { register } from './components/register.js' 4 | import { GlobalAPI } from './api/global-api.js' 5 | import { installPlugin } from './components/plugins.js' 6 | import { addMixin } from './components/mixins.js' 7 | import { CreateElement } from './api/create-element' 8 | import { UserComponent, ComponentClass } from './api/component' 9 | import { ComponentProxy } from './api/component-proxy' 10 | import { Plugin } from './api/plugin' 11 | import { VNode } from './api/vnode' 12 | 13 | const globalAPI: GlobalAPI = { 14 | mount: mountRootComponent, 15 | unmount: unmountComponent, 16 | compileTemplate: compileTemplate, 17 | include: register, 18 | register: register, 19 | mixin: addMixin, 20 | install: installPlugin 21 | } 22 | 23 | export { 24 | globalAPI as Nuro, 25 | GlobalAPI, 26 | CreateElement, 27 | UserComponent as Component, 28 | ComponentClass, 29 | ComponentProxy, 30 | Plugin, 31 | VNode 32 | } 33 | -------------------------------------------------------------------------------- /__tests__/unit/string-utils.test.js: -------------------------------------------------------------------------------- 1 | const esmImport = require('esm')(module) 2 | const { camelCaseToKebabCase, isLetter } = esmImport('../../compiled/util/string-utils.js') 3 | 4 | test('should convert camel case to kebab case', () => { 5 | expect(camelCaseToKebabCase('my-string-value')).toEqual('my-string-value') 6 | expect(camelCaseToKebabCase('myStringValue')).toEqual('my-string-value') 7 | expect(camelCaseToKebabCase('UpperCaseStringValue')).toEqual('upper-case-string-value') 8 | expect(camelCaseToKebabCase('ABC')).toEqual('a-b-c') 9 | expect(camelCaseToKebabCase('abc')).toEqual('abc') 10 | expect(camelCaseToKebabCase('a&b')).toEqual('a&b') 11 | expect(camelCaseToKebabCase('a')).toEqual('a') 12 | expect(camelCaseToKebabCase('A')).toEqual('a') 13 | }) 14 | 15 | test('should check if a character is a letter', () => { 16 | expect(isLetter('a')).toEqual(true) 17 | expect(isLetter('A')).toEqual(true) 18 | expect(isLetter('z')).toEqual(true) 19 | expect(isLetter('Z')).toEqual(true) 20 | expect(isLetter('1')).toEqual(false) 21 | expect(isLetter('!')).toEqual(false) 22 | expect(isLetter('aa')).toEqual(false) 23 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 jegan321 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/compiler/attrs-directive.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | let compileTemplate = Nuro.compileTemplate 3 | 4 | test('should take all attributes from object literal', () => { 5 | let code = compileTemplate('
') 6 | expect(code) 7 | .toEqual("with(this){return h('div',{...{},...{id: 123},...{class:({}.class?({}.class+' '+({id: 123}.class||'')).trim():{id: 123}.class)}})}") 8 | }) 9 | 10 | test('should take all attributes from props', () => { 11 | let code = compileTemplate('
') 12 | expect(code) 13 | .toEqual("with(this){return h('div',{...{},...props,...{class:({}.class?({}.class+' '+(props.class||'')).trim():props.class)}})}") 14 | }) 15 | 16 | test('should use some hard coded attribues and rest from props', () => { 17 | let code = compileTemplate('
') 18 | expect(code) 19 | .toEqual("with(this){return h('div',{...{'id':'123','class':'my-class'},...props,...{class:({'id':'123','class':'my-class'}.class?({'id':'123','class':'my-class'}.class+' '+(props.class||'')).trim():props.class)}})}") 20 | }) -------------------------------------------------------------------------------- /__tests__/compiler/comments.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | let compileTemplate = Nuro.compileTemplate 3 | 4 | test('should compile div with comment', () => { 5 | let code = compileTemplate(`
`) 6 | expect(code) 7 | .toEqual("with(this){return h('div',{})}") 8 | }) 9 | 10 | test('should compile div with comment and paragraph', () => { 11 | let code = compileTemplate(`

Hello

`) 12 | expect(code) 13 | .toEqual("with(this){return h('div',{},[h('p',{},['Hello'])])}") 14 | }) 15 | 16 | test('should compile div with comment and other children', () => { 17 | let code = compileTemplate(`

Title

Hello

`) 18 | expect(code) 19 | .toEqual("with(this){return h('div',{},[h('h1',{},['Title']),h('p',{},['Hello'])])}") 20 | }) 21 | 22 | test('should compile div with multiple comments and other children', () => { 23 | let code = compileTemplate(`

Title

Hello

`) 24 | expect(code) 25 | .toEqual("with(this){return h('div',{},[h('h1',{},['Title']),h('p',{},['Hello'])])}") 26 | }) -------------------------------------------------------------------------------- /__tests__/integration/state.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | 3 | test('simple state', () => { 4 | document.body.innerHTML = '
' 5 | 6 | class TestComponent { 7 | foo = 'foo value' 8 | bar = 'bar value' 9 | render($) { 10 | return $('div', {id: 'app', 'data-foo': this.foo}, [ 11 | this.bar 12 | ]) 13 | } 14 | } 15 | Nuro.mount(TestComponent, window.document.querySelector('#target')) 16 | 17 | expect(document.getElementById('app').outerHTML) 18 | .toEqual(`
bar value
`) 19 | }) 20 | 21 | test('array in state', () => { 22 | document.body.innerHTML = '
' 23 | 24 | class TestComponent { 25 | things = [{desc: 'one'}, {desc: 'two'}] 26 | render($) { 27 | return $('div', {id: 'app'}, this.things.map(thing => { 28 | return $('button', {}, [ 29 | thing.desc 30 | ]) 31 | })) 32 | } 33 | } 34 | Nuro.mount(TestComponent, window.document.querySelector('#target')) 35 | 36 | expect(document.getElementById('app').outerHTML) 37 | .toEqual(`
`) 38 | }) -------------------------------------------------------------------------------- /src/api/component.ts: -------------------------------------------------------------------------------- 1 | import { CreateElement } from './create-element.js' 2 | import { VNode } from './vnode.js' 3 | 4 | export abstract class UserComponent { 5 | template?: string 6 | includes?: Record 7 | props: Props 8 | constructor(props: Props) { 9 | this.props = props 10 | } 11 | beforeInit() {} 12 | beforeMount() {} 13 | afterMount() {} 14 | beforeRender() {} 15 | afterRender() {} 16 | beforeUnmount() {} 17 | } 18 | 19 | interface BaseComponent { 20 | [state: string]: any 21 | props?: Record 22 | beforeInit?: () => void 23 | beforeMount?: () => void 24 | afterMount?: () => void 25 | beforeRender?: () => void 26 | afterRender?: () => void 27 | beforeUnmount?: () => void 28 | } 29 | 30 | export interface RenderComponent extends BaseComponent { 31 | render: (createElement: CreateElement) => VNode 32 | } 33 | 34 | export interface Render { 35 | (createElement: CreateElement): VNode 36 | } 37 | 38 | export interface TemplateComponent extends BaseComponent { 39 | template: string 40 | includes?: Record 41 | } 42 | 43 | export type Component = RenderComponent | TemplateComponent 44 | 45 | export interface ComponentClass { 46 | new (props: any): Component 47 | } 48 | -------------------------------------------------------------------------------- /__tests__/compiler/for-directive.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | let compileTemplate = Nuro.compileTemplate 3 | 4 | test('should compile $for directive with hard coded text', () => { 5 | let code = compileTemplate('

Thing

') 6 | expect(code) 7 | .toEqual("with(this){return h('div',{},[...things.map((thing)=>h('p',{},['Thing']))])}") 8 | }) 9 | 10 | test('should compile $for directive with dynamic text', () => { 11 | let code = compileTemplate('

{{thing}}

') 12 | expect(code) 13 | .toEqual("with(this){return h('div',{},[...things.map((thing)=>h('p',{},[(thing)]))])}") 14 | }) 15 | 16 | test('should compile $for directive with var and index', () => { 17 | let code = compileTemplate('

{{thing+i}}

') 18 | expect(code) 19 | .toEqual("with(this){return h('div',{},[...things.map((thing, i)=>h('p',{},[(thing+i)]))])}") 20 | }) 21 | 22 | test('should compile $for directive with dynamic attributes', () => { 23 | let code = compileTemplate('

{{thing}}

') 24 | expect(code) 25 | .toEqual("with(this){return h('div',{},[...things.map((thing)=>h('p',{'data-thing':thing},[(thing)]))])}") 26 | }) -------------------------------------------------------------------------------- /src/util/object-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Holder for two generic values 3 | */ 4 | interface Pair { 5 | left: T 6 | right: V 7 | } 8 | 9 | /** 10 | * Creates a new array made up of pairs. Each pair is the element from each 11 | * array at that index. 12 | * 13 | * For example, index 1 is xs[1] and ys[1], index 2 is xs[2] and ys[2], etc. 14 | * 15 | * If they have different lengths, the new array will 16 | * be the length of the shortest one. 17 | */ 18 | export function zip(xs: ArrayLike, ys: ArrayLike): Pair[] { 19 | const zipped = [] 20 | for (let i = 0; i < Math.max(xs.length, ys.length); i++) { 21 | let pair = { 22 | left: xs[i], 23 | right: ys[i] 24 | } 25 | zipped.push(pair) 26 | } 27 | return zipped 28 | } 29 | 30 | export function getMethodNames(Class: any): string[] { 31 | return Object.getOwnPropertyNames(Class.prototype).filter(x => x !== 'constructor') 32 | } 33 | 34 | export function isObject(val: unknown): val is Object { 35 | return Object.prototype.toString.call(val) === '[object Object]' 36 | } 37 | 38 | export function isFunction(val: unknown): val is Function { 39 | return Object.prototype.toString.call(val) === '[object Function]' 40 | } 41 | 42 | export function isArray(val: unknown): val is Array { 43 | return Array.isArray(val) 44 | } 45 | -------------------------------------------------------------------------------- /__tests__/unit/object-utils.test.js: -------------------------------------------------------------------------------- 1 | const esmImport = require('esm')(module) 2 | const { isObject, isArray, isFunction } = esmImport('../../compiled/util/object-utils.js') 3 | 4 | test('should test whether a value is an object', () => { 5 | expect(isObject({})).toEqual(true) 6 | expect(isObject({foo: 'bar'})).toEqual(true) 7 | 8 | expect(isObject('foo')).toEqual(false) 9 | expect(isObject(true)).toEqual(false) 10 | expect(isObject(123)).toEqual(false) 11 | expect(isObject(null)).toEqual(false) 12 | expect(isObject([])).toEqual(false) 13 | expect(isObject(()=>{})).toEqual(false) 14 | }) 15 | 16 | test('should test whether a value is an array', () => { 17 | expect(isArray([])).toEqual(true) 18 | expect(isArray(['foo'])).toEqual(true) 19 | 20 | expect(isArray('foo')).toEqual(false) 21 | expect(isArray(true)).toEqual(false) 22 | expect(isArray(123)).toEqual(false) 23 | expect(isArray(null)).toEqual(false) 24 | expect(isArray({})).toEqual(false) 25 | expect(isArray(()=>{})).toEqual(false) 26 | }) 27 | 28 | test('should test whether a value is a function', () => { 29 | expect(isFunction(()=>{})).toEqual(true) 30 | expect(isFunction(function(){})).toEqual(true) 31 | 32 | expect(isFunction('foo')).toEqual(false) 33 | expect(isFunction(true)).toEqual(false) 34 | expect(isFunction(123)).toEqual(false) 35 | expect(isFunction(null)).toEqual(false) 36 | expect(isFunction({})).toEqual(false) 37 | expect(isFunction([])).toEqual(false) 38 | }) -------------------------------------------------------------------------------- /src/dom/map-vnode.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from '../api/vnode.js' 2 | 3 | /** 4 | * Builds a VNode instance based on a given Element 5 | * @param {Node} rootNode 6 | */ 7 | export function mapVNode(rootNode: Element, includeComments: boolean = true): VNode { 8 | return createVNode(rootNode, includeComments) 9 | } 10 | 11 | function createVNode(node: Element, includeComments: boolean): VNode { 12 | if (node.nodeType === 1) { 13 | // Node is an element 14 | let vNode: VNode = { 15 | nodeType: 'element', 16 | tag: node.tagName.toLowerCase(), 17 | text: '', 18 | attrs: {}, 19 | children: [] 20 | } 21 | Array.prototype.forEach.call(node.attributes, attr => { 22 | vNode.attrs[attr.name] = attr.value 23 | }) 24 | vNode.children = createChildren(node.childNodes, includeComments) 25 | return vNode 26 | } else { 27 | // Node is text or comment 28 | return { 29 | nodeType: node.nodeType === 8 ? 'comment' : 'text', 30 | text: node.textContent || '', 31 | tag: '', 32 | attrs: {}, 33 | children: [] 34 | } 35 | } 36 | } 37 | 38 | function createChildren(children: NodeListOf, includeComments: boolean): VNode[] { 39 | let vChildren: VNode[] = [] 40 | Array.prototype.forEach.call(children, child => { 41 | if (includeComments || child.nodeType !== 8) { 42 | let vNode = createVNode(child, includeComments) 43 | vChildren.push(vNode) 44 | } 45 | }) 46 | return vChildren 47 | } 48 | -------------------------------------------------------------------------------- /__tests__/compiler/bind-directive.test.js: -------------------------------------------------------------------------------- 1 | let Nuro = require('../../dist/nuro.umd.js') 2 | let compileTemplate = Nuro.compileTemplate 3 | 4 | test('should compile $bind directive on input', () => { 5 | let code = compileTemplate('') 6 | expect(code) 7 | .toEqual(`with(this){return h('input',{'value':name,'@input':(e)=>{this.$update({name:e.target.value})}})}`) 8 | }) 9 | 10 | test('should compile $bind directive on text input', () => { 11 | let code = compileTemplate('') 12 | expect(code) 13 | .toEqual(`with(this){return h('input',{'type':'input','value':name,'@input':(e)=>{this.$update({name:e.target.value})}})}`) 14 | }) 15 | 16 | test('should compile $bind directive on textarea', () => { 17 | let code = compileTemplate('