4.0.27 • Published 7 months ago

reacton-js v4.0.27

Weekly downloads
-
License
MIT
Repository
-
Last release
7 months ago

EN / RU

reacton

GitHub | GitFlic | GitVerse | NpmJS | Download⤵️

Reacton (short Rtn) is a JavaScript framework for quickly creating reactive Web Components. It supports all the methods and properties that are provided by standard Web components. In addition, the framework contains a number of additional methods and implements server-side rendering of Web components.

Below is an example of creating a simple component:

class WHello {
  // initializing the properties of a state object
  message = 'Reacton'
  color = 'orangered'

  static mode = 'open' // add Shadow DOM

  // return the HTML markup of the component
  static template = `
    <h1>Hello, {{ message }}!</h1>
    
    <style>
      h1 {
        color: {{ color }};
      }
    </style>
  `
}
  1. Quick start
  2. Component state
  3. Cycles
  4. Mixins
  5. Views
  6. Reactive properties
  7. Static properties
  8. Special methods
  9. Event Emitter
  10. Router
  11. Server-side rendering

Classes are used to create components. Classes can be either built into the main script or imported from an external module. Create a new working directory, for example named app, and download the rtn.global.js file into this directory.

Add an index.html file to the directory with the following content:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- connect Hello component to the document -->
  <w-hello></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // initializing the properties of a state object
      message = 'Reacton'
      color = 'orangered'

      static mode = 'open' // add Shadow DOM

      // return the HTML markup of the component
      static template = `
        <h1>Hello, {{ message }}!</h1>
        
        <style>
          h1 {
            color: {{ color }};
          }
        </style>
      `
    }

    // pass the class of the Hello component to the Rtn function
    Rtn(WHello)
  </script>
</body>
</html>

To ensure there are no naming conflicts between standard and custom HTML elements, the component name must contain a dash «-», for example, "my-element" and "super-button" are valid names, but "myelement" is not.

In most of the examples in this guide, the prefix will consist of a single letter «w-». that is, the Hello component will be called "w-hello".

When defining a component class, its prefix and name must begin with a capital letter. WHello is the correct class name, but wHello is not.

When you open the index.html file in the browser, the screen will display the message created in the Hello component:

The components can be placed in separate modules. In this case, the Hello component file would look like the following:

export default class WHello {
  // initializing the properties of a state object
  message = 'Reacton'
  color = 'orangered'

  static mode = 'open' // add Shadow DOM

  // return the HTML markup of the component
  static template = `
    <h1>Hello, {{ message }}!</h1>
    
    <style>
      h1 {
        color: {{ color }};
      }
    </style>
  `
}

To work with external components, you will need any development server, such as lite-server.

You can install this server using the command in the terminal:

npm install --global lite-server

The server is started from the directory where the application is located using a command in the terminal:

lite-server

In addition, the framework supports single-file components that can be used along with modular ones when creating a project in the webpack build system.

An example of a simple single-file component is shown below:

<h1>Hello, {{ message }}!</h1>
      
<style>
  h1 {
    color: {{ color }};
  }
</style>

<script>
  exports = class WHello {
    // initializing the properties of a state object
    message = 'Reacton'
    color = 'orangered'

    static mode = 'open' // add Shadow DOM
  }
</script>

A single-file component must assign its class to the exports variable. This variable will be automatically declared during the creation of the component structure in the project's build system.

In single-file components, you can use the import instruction, for example:

<script>
  // import default object from module
  import obj from './module.js'

  exports = class WHello {
    // initializing the properties of a state object
    message = obj.message
    color = obj.color

    static mode = 'open' // add Shadow DOM
  }
</script>

Single-file components allow you to separate HTML markup from component logic. However, such components cannot work directly in the browser. They require a special handler that connects to the webpack.

To be able to work in the browser with components in which logic is separated from HTML content, there are built-in components.

An example of a simple embedded component is shown below:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- connect Hello component to the document -->
  <w-hello></w-hello>

  <!-- define the template of the Hello component -->
  <template id="tempHello">
    <h1>Hello, {{ message }}!</h1>
          
    <style>
      h1 {
        color: {{ color }};
      }
    </style>

    <script>
      return class WHello {
        // initializing the properties of a state object
        message = 'Reacton'
        color = 'orangered'

        static mode = 'open' // add Shadow DOM
      }
    </script>
  </template>

  <script src="rtn.global.js"></script>

  <script>
    // pass the template of the Hello component to the Rtn function
    Rtn(tempHello)
  </script>
</body>
</html>

The embedded component should return its class, and the contents of its <script> tag can be considered as a function. However, embedded components are not suitable for server-side rendering and, in addition, they cannot use the import instruction, but it is allowed to use the expression import(), for example:

<script>
  // import a module and save its object in a variable
  let obj = await import('./module.js')

  return class WHello {
    // initializing the properties of a state object
    message = obj.message
    color = obj.color

    static mode = 'open' // add Shadow DOM
  }
</script>

For quick access to the component, it is enough to add an identifier to the element that connects the component to the document, as shown below:

<!-- connect Hello component to the document -->
<w-hello id="hello"></w-hello>

Now open the browser console and enter the commands sequentially:

hello.$state.message = 'Web Components'
hello.$state.color = 'blue'

The color and content of the header will change:

Each component can contain changing data, which is called a state. The state can be defined in the constructor of the component class:

class WHello {
  constructor() {
    // initializing the properties of a state object
    this.message = 'Reacton'
    this.color = 'orangered'
  }
  ...
}

Alternatively, using the new syntax, you can define the state directly in the class itself:

class WHello {
  // initializing the properties of a state object
  message = 'Reacton'
  color = 'orangered'
  ...
}

The methods of a component are not a state. They are designed to perform actions with the state of the component and are stored in the prototype of the state object:

class WHello {
  // initializing the property of a state object
  message = 'Reacton'

  // define the method of the state object
  printStr(str) {
    return this.message
  }

  // return the HTML markup of the component
  static template = `<h1>Hello, {{ printStr() }}!</h1>`
}

The special property $state is used to access the state object. Using this property, you can get or assign a new value to the state, as shown below:

hello.$state.message = 'Web Components'

The component content is updated automatically based on the new state.

When the content of a component is updated, its old DOM is not deleted. This means that the handlers assigned to the elements inside the component are preserved, since the old element is not replaced by a new element.

In the example below, the handler for the <h1> element will still work after the component state is updated. Because the update will only change the old value of its attribute and text content:

class WHello {
  // initializing the property of a state object
  message = 'Reacton'

  /* this method is performed after connecting the component to the document
    when the DOM has already been created for the component from which you can select elements */
  static connected() {
    this.$('h1').addEventListener('click', e => console.log(e.target))
  }

  // return the HTML markup of the component
  static template = `<h1 :title="message">Hello, {{ message }}!</h1>`
}

Reacton supports three kinds of «for» loops that are implemented in JavaScript. They are all defined with a special $for attribute and output the contents of their HTML elements as many times as required by the loop condition.

This attribute will not be displayed in the compiled component.

In the example below, the «for» loop outputs 10 paragraphs with numbers from 0 to 9:

class WHello {
  // return the HTML markup of the component
  static template = `
    <div $for="i = 0; i < 10; i++">
      <p>Number: {{ i }}</p>
    </div>
  `
}

In the special attribute $for you cannot use variable definition operators: var, let and const respectively. This will result in an error:

static template = `
  <div $for="let i = 0; i < 10; i++">
    <p>Number: {{ i }}</p>
  </div>
`

The «for-in» loop is used to output the contents of objects, as shown below:

class WHello {
  // initializing the property of a state object
  user = {
    name: 'John',
    age: 32
  }

  // return the HTML markup of the component
  static template = `
    <ul $for="prop in user">
      <li>
        <b>{{ prop }}</b>: {{ user[prop] }}
      </li>
    </ul>
  `
}

The «for-of» loop is designed to work with arrays:

class WHello {
  // initializing the property of a state object
  colors = ['red', 'green', 'blue']

  // return the HTML markup of the component
  static template = `
    <ul $for="col of colors">
      <li>{{ col }}</li>
    </ul>
  `
}

When using events in loops using the special @event attribute, they will use the current value of the loop variable for their iteration phase:

static template = `
  <ul $for="col of colors">
    <li @click="console.log(col)">{{ col }}</li>
  </ul>
`

More details about these events and other special attributes will be discussed later in the guide.

You can use loops with any nesting depth:

class WHello {
  // initializing the property of a state object
  users = [
    {
      name: 'John',
      age: 32,
      skills: {
        frontend: ['HTML', 'CSS'],
        backend: ['Ruby', 'PHP', 'MySQL']
      }
    },
    {
      name: 'Clementine',
      age: 25,
      skills: {
        frontend: ['HTML', 'JavaScript'],
        backend: ['PHP']
      }
    },
    {
      name: 'Chelsey',
      age: 30,
      skills: {
        frontend: ['HTML', 'CSS', 'JavaScript', 'jQuery'],
        backend: ['Ruby', 'MySQL']
      }
    }
  ]

  // return the HTML markup of the component
  static template = `
    <div $for="user of users">
      <div>
        <p>
          <b>Name</b>: {{ user.name }}
        </p>
        <p>
          <b>Age</b>: {{ user.age }}
        </p>
        <div $for="category in user.skills">
          <b>{{ category[0].toUpperCase() + category.slice(1) }}</b>:
          <ol $for="item of user.skills[category]">
            <li>{{ item }}</li>
          </ol>
        </div>
      </div>
      <hr>
    </div>
  `
}

Mixin is a general term in object-oriented programming: a class that contains methods for other classes. These methods can use different components, which eliminates the need to create methods with the same functionality for each component separately.

In the example below, the mixin's printName() method is used by the Hello and Goodbye components:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- connect Hello component to the document -->
  <w-hello></w-hello>

   <!-- connect Goodbye component to the document -->
  <w-goodbye></w-goodbye>

  <script src="rtn.global.js"></script>

  <script>
    // define a Mixin class for common methods
    class Mixin {
      printName() {
        return this.userName
      }
    }

    // extend the Hello component class from the Mixin class
    class WHello extends Mixin {
      // initializing the property of a state object
      userName = 'Anna'

      // return the HTML markup of the component
      static template = `<h1>Hello, {{ printName() }}!</h1>`
    }

    // extend the Goodbye component class from the Mixin class
    class WGoodbye extends Mixin {
      // initializing the property of a state object
      userName = 'John'

      // return the HTML markup of the component
      static template = `<p>Goodbye, {{ printName() }}...</p>`
    }

    // pass the Hello and Goodbye component classes to the Rtn function
    Rtn(WHello, WGoodbye)
  </script>
</body>
</html>

To display various components, a special attribute $view is used. This attribute can be assigned to any element, but usually the DIV element is used. The element containing the attribute is replaced by the component whose name is contained in the value of this attribute, for example:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
   <!-- connect WContent component to the document -->
  <w-content></w-content>

  <script src="rtn.global.js"></script>

  <script>
    class WContent {
      // initializing the property of a state object
      compName = 'w-hello'

      // define the method of the state object
      changeView() {
        this.compName = this.compName === 'w-hello' ? 'w-goodbye' : 'w-hello'
      }
      
      // return the HTML markup of the component
      static template = `
        <div $view="compName"></div>
        <button @click="changeView">Switch</button>
      `
    }

    class WHello {
      // initializing the property of a state object
      userName = 'Anna'

      // return the HTML markup of the component
      static template = `<h1>Hello, {{ userName }}!</h1>`
    }

    class WGoodbye {
      // initializing the property of a state object
      userName = 'John'

      // return the HTML markup of the component
      static template = `<p>Goodbye, {{ userName }}...</p>`
    }
    
    // pass component classes to Rtn function
    Rtn(WContent, WHello, WGoodbye)
  </script>
</body>
</html>

The $view attribute cannot be used with loops. The example below will result in an error:

static template = `
  <div $view="compName" $for="i = 0; i < 10; i++">
    <p>Number: {{ i }}</p>
  </div>
`

All state object properties used in a component are reactive, meaning that when their value changes, the values ​​in all places in the component's HTML markup where these properties are used also change.

To insert reactive properties into text nodes, use double curly braces:

static template = `
  <h1>Hello, {{ message }}!</h1>
  
  <style>
    h1 {
      color: {{ color }};
    }
  </style>
`

To insert a reactive property into an attribute, you must precede its name with a colon:

static template = `<h1 :title="message">Hello, Reacton!</h1>`

In the example below, a reactive property is added to a boolean attribute:

class WHello {
  // initializing the property of a state object
  hide = true

  // return the HTML markup of the component
  static template = `<h1 :hidden="hide">Hello, Reacton!</h1>`
}

The colon before the attribute name is used only in the component template HTML markup to indicate that the attribute accepts a reactive property. After compilation, the resulting component markup will display the attribute names without the colons.

For event attributes, the attribute name is preceded by the @ symbol, followed by the event name without the on prefix, as shown below:

class WHello {
  // initializing the property of a state object
  hide = true

  // return the HTML markup of the component
  static template = `
    <h1 :hidden="hide">Hello, Reacton!</h1>
    <button @click="hide = !hide">Hide/Show</button>
  `
}

Instead of directly changing the reactive property in the event attribute, you can pass the name of a method that changes the reactive property, for example:

class WHello {
  // initializing the property of a state object
  hide = true

  // define the method of the state object
  changeHide() {
    this.hide = !this.hide
  }

  // return the HTML markup of the component
  static template = `
    <h1 :hidden="hide">Hello, Reacton!</h1>
    <button @click="changeHide">Hide/Show</button>
  `
}

The event attributes contain an event object, using the target property of which you can get a reference to the element on which the event occurred:

static template = `<button @click="console.log(event.target)">Show in console</button>`

Event attributes can have the same parameters that are passed in the third argument to the addEventListener method. These parameters are specified separated by a dot after the event name:

@click.once.capture.passive

In the example below, the element that triggers the event will only be shown in the console once:

static template = `<button @click.once="console.log(event.target)">Show in console</button>`

alias – this static property allows you to add an alias for the this keyword, i.e. for the context of the state object:

class WHello {
  // initializing the property of a state object
  message = 'Reacton'

  static alias = 'o' // add alias for "this"

  // return the HTML markup of the component
  static template = `<h1>Hello, {{ o.message }}!</h1>`
}

*By default, there is no need to add the this keyword before the state object property names. However, if an alias is added, then either the alias or this keyword must be used before the property and method names.*

time – this static property allows you to add a component refresh timer if you set it to "true" as shown below:

class WHello {
  // initializing the property of a state object
  message = 'Reacton'

  static time = true // add refresh timer

  // return the HTML markup of the component
  static template = `<h1>Hello, {{ message }}!</h1>`
}

The component update time in milliseconds is displayed in the console after each change to any property of the state object.

name – this static property used, for example, when an anonymous class is passed to the Rtn function, as shown below:

// pass the anonymous class of the Hello component to the Rtn function
Rtn(class {
  // initializing the property of a state object
  message = 'Reacton'

  static name = 'w-hello' // name of the component

  // return the HTML markup of the component
  static template = `<h1>Hello, {{ message }}!</h1>`
})

mode – this static property responsible for adding a Shadow DOM to the component. It can contain two values: "open" or "closed". In the latter case, when the component is closed, it is impossible to access the properties of its state object, methods for selecting elements and updating the content from the console.

Access to the properties of the state object, methods for selecting and updating the content of the component, in closed components is possible only from static methods, for example:

class WHello {
  static mode = 'closed' // add a closed Shadow DOM

  // it is performed at the end of connecting the component to the document
  static connected() {
    // get an element using the sampling method
    const elem = this.$('h1')

    // add an event handler to the element
    elem.addEventListener('click', e => console.log(e.target))
  }

  // initializing the property of a state object
  message = 'Reacton'

  // return the HTML markup of the component
  static template = `<h1>Hello, {{ message }}!</h1>`
}

Only components with a Shadow DOM can contain local styles.

extends – this static property responsible for creating customized components, i.e. those that are embedded in standard HTML elements, for example:

<body>
  <!-- embed the Hello component in the header element -->
  <header is="w-hello"></header>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // initializing the property of a state object
      message = 'Reacton'

      static extends = 'header' // the name of the embedded element

      // return the HTML markup of the component
      static template = `<h1>Hello, {{ message }}!</h1>`
    }

    // pass the class of the Hello component to the Rtn function
    Rtn(WHello)
  </script>
</body>

The property must contain the name of the embedded element, and the embedded element itself must contain the is attribute with a value equal to the name of the component embedded in it.

serializable – this static property responsible for serializing the Shadow DOM of the component using the getHTML() method. By default, it has the value "false".

template() – this static property returns the future HTML content of the component as a string:

// return the HTML markup of the component
static template = `
  <h1>Hello, {{ message }}!</h1>
  
  <style>
    h1 {
      color: {{ color }};
    }
  </style>
`

startConnect() – this static method is executed at the very beginning of connecting the component to the document, before generating the HTML content of the component and calling the static connected() method, but after creating the component state object.

In it, you can initialize the properties of the state object with the existing values:

class WHello {
  // it is performed at the beginning of connecting the component to the document
  static startConnect() {
    // initializing the property of a state object
    this.message = 'Reacton'
  }

  // return the HTML markup of the component
  static template = `<h1>Hello, {{ message }}!</h1>`
}

or get data from the server to initialize their. But in this case, the method must be asynchronous:

class WHello {
  // it is performed at the beginning of connecting the component to the document
  static async startConnect() {
    // initializing the state object property with data from a conditional server
    this.message = await new Promise(ok => setTimeout(() => ok('Reacton'), 1000))
  }

  // return the HTML markup of the component
  static template = `<h1>Hello, {{ message }}!</h1>`
}

This is the only static method that can be asynchronous.

connected() – this static method is executed at the very end of connecting the component to the document, after generating the HTML content of the component and calling the static startConnect() method.

In it, you can add event handlers to the internal elements of the component:

class WHello {
  // initializing the property of a state object
  message = 'Reacton'

  // it is performed at the end of connecting the component to the document
  static connected() {
    // get an element using the sampling method
    const elem = this.$('h1')

    // add an event handler to the element
    elem.addEventListener('click', e => console.log(e.target))
  }

  // return the HTML markup of the component
  static template = `<h1>Hello, {{ message }}!</h1>`
}

This and all subsequent static methods are abbreviations of the standard static methods of the component.

disconnected() – this static method is executed when a component is removed from a document.

adopted() – this static method is executed when the component is moved to a new document.

changed() – this static method is executed when one of the monitored attributes is changed.

attributes – this static array contains the names of the monitored attributes, for example:

<body>
  <!-- connect Hello component to the document -->
  <w-hello data-message="Reacton"></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // it is performed at the beginning of connecting the component to the document
      static startConnect() {
        // initializing the property of a state object
        this.message = this.$data.message
      }
      
      // it is performed at the end of connecting the component to the document
      static connected() {
        // get an element using the sampling method
        const elem = this.$('h1')

        // add an event handler to the element
        elem.addEventListener('click', e => this.$data.message = 'Web components')
      }

      // it is executed when one of the monitored attributes is changed
      static changed(name, oldValue, newValue) {
        // if the new attribute value is not equal to the old value 
        if (newValue !== oldValue) {
          // change the value of a state object property
          this.message = newValue
        }
      }

      // contains the names of the monitored attributes
      static attributes = ['data-message']

      // return the HTML markup of the component
      static template = `<h1>Hello, {{ message }}!</h1>`
    }

    // pass the class of the Hello component to the Rtn function
    Rtn(WHello)
  </script>
</body>

All static methods are called in the context of the proxy of the component state object. This means that if the required property is not found in the state object, then the search takes place in the component itself.

In the example below, the id property does not exist in the component state object. Therefore, it is requested from the component itself:

<body>
  <!-- connect Hello component to the document -->
  <w-hello id="hello"></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // return the HTML markup of the component
      static template = `<h1>Hello, the component with the ID {{ id }}!</h1>`
    }

    // pass the class of the Hello component to the Rtn function
    Rtn(WHello)
  </script>
</body>

All special methods and properties start with the dollar symbol «$» followed by the name of the method or property.

\$() – this special method selects an element from the component content by the specified selector, for example, to add an event handler to the element:

// it is performed at the end of connecting the component to the document
static connected() {
  // get an element using the sampling method
  const elem = this.$('h1')

  // add an event handler to the element
  elem.addEventListener('click', e => console.log(e.target))
}

This method fetches the contents of private components only if it is called from static methods of the component class.

\$$() – this special method selects all elements from the component content by the specified selector, for example, to add event handlers to the elements when iterating through them in a loop:

// it is performed at the end of connecting the component to the document
static connected() {
  // get all elements using the sampling method
  const elems = this.$$('h1')

  // iterate through a collection of elements in a loop
  for (const elem of elems) {
    // add an event handler to the element
    elem.addEventListener('click', e => console.log(e.target))
  }
}

This method fetches the contents of private components only if it is called from static methods of the component class.

\$entities() – this special method neutralizes a string containing HTML content obtained from unreliable sources. By default, the ampersand character «&» is escaped, characters less than «<» and more than «>», double «"» and single quotes «'», for example:

class WHello {
  // it is performed at the beginning of connecting the component to the document
  static async startConnect() {
    // getting HTML content from a conditional server
    const html = await new Promise(ok => setTimeout(() => ok('<script>dangerous code<\/script>'), 1000))

    // initialization of a state object property with neutralized HTML content
    this.message = this.$entities(html)
  }

  // return the HTML markup of the component
  static template = `{{ message }}`
}

In addition to the above characters, you can escape any characters by passing an array in the second and subsequent arguments of the form: regular expression, replacement string, for example:

this.$entities(html, [/\(/g, '&lpar;'], [/\)/g, '&rpar;'])

This method is available as a property of the Rtn function, as shown below:

Rtn.entities(html)

or named import when using the modular version of the framework:

<body>
  <!-- connect Hello component to the document -->
  <w-hello></w-hello>

  <script type="module">
    import Rtn, { Entities } from "./rtn.esm.js"

    class WHello {
      // return the HTML markup of the component
      static template = `${ Entities('<script>dangerous code<\/script>') }`
    }

    // pass the class of the Hello component to the Rtn function
    Rtn(WHello)
  </script>
</body>

The special methods: \$event(), \$router() and \$render() will be discussed in the following sections. As with the \$entities() method, they also have their own named imports:

import Rtn, { Tag, Event, Router, Render } from "./rtn.esm.js"

The Rtn function is always imported by default.

\$state – this special property refers to the proxy of the component's state object. This means that if the required property is not found in the state object, the search occurs in the component itself.

In the example below, the id property does not exist in the component state object. Therefore, it is requested from the component itself:

<body>
  <!-- connect Hello component to the document -->
  <w-hello id="hello"></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // return the HTML markup of the component
      static template = `<h1>Hello, component with ID {{ id }}!</h1>`
    }

    // pass the class of the Hello component to the Rtn function
    Rtn(WHello)
  </script>
</body>

\$host – this special property refers to the element that connects the component to the document, i.e. the component element. This can be useful if properties with the same name are present in both the state object and the component..

The proxy of the state object initially looks for a property in the state object itself, which means that to get the property of the same name from the component element, you must use the special property $host, as shown below:

<body>
  <!-- connect Hello component to the document -->
  <w-hello id="hello"></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // initializing the property of a state object
      id = 'Reacton'

      // return the HTML markup of the component
      static template = `
        <h1>Hello, the ID property with the value {{ id }}!</h1>
        <h2>Hello, component with ID {{ $host.id }}!</h2>
      `
    }

    // pass the class of the Hello component to the Rtn function
    Rtn(WHello)
  </script>
</body>

\$shadow – this special property refers to the Shadow DOM of the component:

hello.$shadow

For closed components and components without a Shadow DOM, this property returns "null".

\$data – this special property refers to the component's dataset object, which is used to access custom attributes, for example:

<body>
  <!-- connect Hello component to the document -->
  <w-hello data-message="Reacton"></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // return the HTML markup of the component
      static template = `<h1>Hello, {{ $data.message }}!</h1>`
    }

    // pass the class of the Hello component to the Rtn function
    Rtn(WHello)
  </script>
</body>

\$props – this special property refers to the state object of the parent component when the special attribute \$props without a value is passed to the child component:

<body>
  <!-- connect Hello component to the document -->
  <w-hello></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    // parent component Hello
    class WHello {
       // initializing the properties of a state object
      message = 'Reacton'
      color = 'orangered'

      // return the HTML markup of the component
      static template = `<w-inner $props></w-inner>`
    }

    // child component Inner
    class WInner {
      static mode = 'open' // add Shadow DOM

      // return the HTML markup of the component
      static template = `
        <h1>Hello, {{ $props.message }}!</h1>
        
        <style>
          h1 {
            color: {{ $props.color }};
          }
        </style>
      `
    }

    // pass the Hello and Inner component classes to the Rtn function
    Rtn(WHello, WInner)
  </script>
</body>

*The special attribute \$props is specified here without any value:*

// return the HTML markup of the component
static template = `<w-inner $props></w-inner>`

To access the properties of the parent component's state object in the child component's HTML markup, a special property \$props is used, as shown below:

// return the HTML markup of the component
static template = `
  <h1>Hello, {{ $props.message }}!</h1>
  
  <style>
    h1 {
      color: {{ $props.color }};
    }
  </style>
`

If it is necessary to transfer only some properties from the parent component, and not the entire state object, then the special attribute \$props must contain a value in which the names of the properties to be transferred are specified, separated by commas:

// return the HTML markup of the component
static template = `<w-inner $props="message, color"></w-inner>`

You can change the state of external components via the special property \$props of internal ones, but not vice versa. Because here we use one-way communication between components:

// parent component Hello
class WHello {
  // initializing the property of a state object
  message = 'Reacton'

  // return the HTML markup of the component
  static template = `
    <h1>Hello, {{ message }}!</h1>
    <w-inner $props="message"></w-inner>
  `
}

// child component Inner
class WInner {
  // return the HTML markup of the component
  static template = `<button @click="$props.message='Web Components'">Change</button>`
}

In order for any components to be able to change data in any other components, custom events are used, which will be discussed further.

To enable components to interact with each other and exchange data, custom events are used. To create custom events, a special $event() method is used, which is available as a property of the Rtn function.

If the method is called as a constructor, it returns a new emitter object that will generate and track user events, for example:

const emit = new Rtn.event()

An ordinary fragment of a document acts as an emitter. You can create as many new emitters as you want, and each emitter can generate and track as many new user events as you want.

When the $event() method is called as a regular function, it receives an emitter in the first argument, the name of the user event is passed in the second, and any data can be passed in the third argument:

this.$event(emit, 'new-array', ['Orange', 'Violet'])

This data will then be available in the custom event handler as the detail property of the Event object, as shown below:

emit.addEventListener('new-array', event => {
  this.rgb = event.detail
})

In the webpack build system, the emitter can be exported from a separate module, for example, from a file Events.js:

import { Event } from 'reacton-js'
export const Emit = new Event()

for the subsequent import of the emitter in the files of the components that will use it:

import { Emit } from './Events'

In the example below, a "click" event handler is added to each button from the Hello component, inside which the corresponding user event of the emitter object is triggered.

To track user events, the emitter is assigned the appropriate handlers in the Colors component. In the last handler, through the detail property of the Event object, a new array is assigned to the state property:

<body>
  <!-- connect Hello component to the document -->
  <w-hello></w-hello>

   <!-- connect Colors component to the document -->
  <w-colors></w-colors>

  <script src="rtn.global.js"></script>

  <script>
    // create a new emitter object
    const emit = new Rtn.event()

    class WHello {
      // return the HTML markup of the component
      static template = `
        <button id="reverse">Reverse an array</button>
        <button id="new-array">New array</button>
      `

      // it is performed at the end of connecting the component to the document
      static connected() {
        // add an event handler to the "Reverse an array" button
        this.$('#reverse').addEventListener('click', () => {
          // initiate the "reverse" event
          this.$event(emit, 'reverse')
        })

        // add an event handler to the "New array" button
        this.$('#new-array').addEventListener('click', () => {
          // initiate the "new-array" event
          this.$event(emit, 'new-array', ['Orange', 'Violet'])
        })
      }
    }

    class WColors {
      // initializing the property of a state object
      rgb = ['Red', 'Green', 'Blue']

      // return the HTML markup of the component
      static template = `
        <ul $for="col of rgb">
          <li>{{ col }}</li>
        </ul>
      `

      // it is performed at the end of connecting the component to the document
      static connected() {
        // add a "reverse" event handler to the emitter
        emit.addEventListener('reverse', () => {
          this.rgb.reverse() // reverse an array
        })

        // add a "new-array" event handler to the emitter
        emit.addEventListener('new-array', event => {
          this.rgb = event.detail // assign a new array to the property
        })
      }
    }

    // pass the Hello and Colors component classes to the Rtn function
    Rtn(WHello, WColors)
  </script>
</body>

The router is based on user events. To create route events, a special method $router() is used, which is available as a property of the Rtn function.

If the method is called as a constructor, it returns a new emitter object with the redefined addEventListener() method, which will generate and track route events, for example:

const emitRouter = new Rtn.router()

When the $router() method is called as a regular function, it receives an emitter in the first argument, the name of the route event is passed in the second, and any data can be passed in the third argument:

this.$router(emitRouter, '/about', ['Orange', 'Violet'])

In a real application, the name of the route event is not specified directly, as in the example above, but is taken from the value of the href attribute of the link that was clicked, for example:

this.$router(emitRouter, event.target.href, ['Orange', 'Violet'])

The user data passed in the last argument of the $router() method will be available in the route event handler as the detail property of the Event object, as shown below:

emitRouter.addEventListener('/about', event => {
  const arr = event.detail
  ...
})

The initial slash «/» in the name of the route event is optional:

emitRouter.addEventListener('about', event => {
  const arr = event.detail
  ...
})

The rest of the name of the route event, except for the initial slash, must completely match the value of the href attribute of the link, after clicking on which the handler corresponding to this value will be triggered:

<a href="/about">About</a>

The difference between user-defined and route events is that the string specified in the route event handler is converted to a regular expression and can contain special regular expression characters, as shown below:

emitRouter.addEventListener('/abou\\w', event => {
  ...
})

In order not to have to use the backslash character twice in a regular string to escape special characters of regular expressions, you can use the tagged function raw() of the built-in String object by enclosing the name of the route event in a template string, for example:

emitRouter.addEventListener(String.raw`/abou\w`, event => {
  ...
})

or so:

const raw = String.raw
emitRouter.addEventListener(raw`/abou\w`, event => {
  ...
})

In addition to the detail property, the Event object has an additional params property to get route parameters, as shown below:

emitRouter.addEventListener('/categories/:catId/products/:prodId', event => {
  const catId = event.params["catId"]
  const prodId = event.params["prodId"]
  ...
})

This handler will be executed for all links of the form:

<a href="/categories/5/products/7">Product</a>

then catId will have the value 5 and prodId will have the value 7.

To support query parameters, the Event object has an additional search property, which is a short reference to the searchParams property of the built-in URL class, for example:

const raw = String.raw
emitRouter.addEventListener(raw`/categories\?catId=\d&prodId=\d`, event => {
  const catId = event.search.get("catId")
  const prodId = event.search.get("prodId")
  ...
})

This handler will be executed for all links of the form:

<a href="/categories?catId=5&prodId=7">Product</a>

then catId will have the value 5 and prodId will have the value 7.

The last addition property of the Event object is called url, which is an object of the built-in URL class and helps parse the request into parts:

emitRouter.addEventListener('/about', event => {
  const hostname = event.url.hostname
  const origin = event.url.origin
  ...
})

Below is an example of creating a simple router with three components for pages:

<body>
   <!-- connect Menu component to the document -->
  <w-menu></w-menu>

   <!-- connect Content component to the document -->
  <w-content></w-content>

  <script src="rtn.global.js"></script>

  <script>
    class WHome {
      // return the HTML markup of the component
      static template = `<h1>Home</h1>`
    }
    class WAbout {
      // return the HTML markup of the component
      static template = `<h1>About</h1>`
    }
    class WContacts {
      // return the HTML markup of the component
      static template = `<h1>Contacts</h1>`
    }

    // create a new emitter object for the router
    const emitRouter = new Rtn.router()

    class WMenu {
      // it is performed at the end of connecting the component to the document
      static connected() {
        // add a "click" event handler for the NAV element
        this.$('nav').addEventListener('click', event => {
          event.preventDefault() // undo the default action
          // initiate an event for the "href" value of the current link
          this.$router(emitRouter, event.target.href)
        })
      }
    
      // return the HTML markup of the component
      static template = `
        <nav>
          <a href="/">Home</a>
          <a href="/about">About</a>
          <a href="/contacts">Contacts</a>
        </nav>
      `
    }

    class WContent {
      // it is performed at the beginning of connecting the component to the document
      static startConnect() {
        // add an event handler to the emitter with an optional route parameter
        emitRouter.addEventListener(`(:page)?`, event => {
          // assign a page component name to the property
          this.page = `w-${event.params.page || 'home'}` 
        })
        
        // initiate an event for the "href" value of the current page
        this.$router(emitRouter, location.href)
      }

      // return the HTML markup of the component
      static template = `<div $view="page"></<div>`
    }

    // pass component classes to the Rtn function
    Rtn(WHome, WAbout, WContacts, WMenu, WContent)
  </script>
</body>

To handle the routes of these pages, the router emitter is assigned a handler with an optional route parameter in the Content component:

// add an event handler to the emitter with an optional route parameter
emitRouter.addEventListener(`(:page)?`, event => {
  // assign a page component name to the property
  this.page = `w-${event.params.page || 'home'}` 
})

In order for this handler to fire immediately when opening the application and connect the page component corresponding to the route, at the end of the connected() static method, an event is triggered for the address of the current route from the href property of the location object:

// initiate an event for the "href" value of the current page
this.$router(emitRouter, location.href)

The rest of the pages components are loaded when you click on the corresponding link in the Menu component:

// add a "click" event handler for the NAV element
this.$('nav').addEventListener('click', event => {
  event.preventDefault() // undo the default action
  // initiate an event for the "href" value of the current link
  this.$router(emitRouter, event.target.href)
})

SSR (Server Side Rendering) is a development technique in which the content of a web page is rendered on the server and not in the client's browser. To render the contents of web pages, the render() method is used, which is available as a property of the Rtn function. This method works both on the server side and in the client's browser.

In the example below, this method outputs the contents of the entire page to the browser console:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- connect Hello component to the document -->
  <w-hello>
    <!-- HTML content transferred to the slot -->
    <span>Reactive Web Components</span>
  </w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // initializing the properties of a state object
      message = 'Reacton'
      color = 'orangered'

      static mode = 'open' // add Shadow DOM

      // return the HTML markup of the component
      static template = `
        <h1>{{ message }} – это <slot></slot></h1>
        
        <style>
          h1 {
            color: {{ color }};
          }
        </style>
      `
    }

    // pass the class of the Hello component to the Rtn function
    Rtn(WHello)

    // output the HTML content of the page to the browser console
    Rtn.render().then(html => console.log(html))
  </script>
</body>
</html>

This method is also available as a named import when using the modular version of the framework:

import { Render } from "./rtn.esm.js"

The method returns a promise, which is resolved after the HTML content of all used components for the current application route is available:

Rtn.render().then(html => console.log(html))

Components of other pages that do not correspond to the current route, if the application uses a router, or components that do not participate in the formation of content when opening the application, will not be taken into account in the promise, otherwise this promise would never have been resolved.

To display content in the browser console not for the entire document, but only starting from a specific element, you must pass an object with the parent parameter to the method, the value of which will be the element from which the output begins.

In the example below, the contents of the document are displayed starting with the BODY element:

Rtn.render({ parent: document.body }).then(html => console.log(html))

By default, the method outputs the cleaned HTML content of the document, i.e. the one in which the tags STYLE, SCRIPT and TEMPLATE have been removed. In order for the method to output the full HTML content, it is necessary to pass it an object with the clean parameter and the value "false", as shown below:

Rtn.render({ clean: false }).then(html => console.log(html))

In all the examples above, the content transferred to the slot was output without the SLOT tags themselves. In order for the transmitted content to be displayed inside these tags, i.e. in full accordance with the structure of the location of this content in the component, the method must pass an object with the slots parameter and the value "true", for example:

Rtn.render({ slots: true }).then(html => console.log(html))

All three parameters can be passed simultaneously:

Rtn.render({
  parent: document.body,
  clean: false,
  slots: true
}).then(html => console.log(html))

The project of the finished application is located at this link. To install all dependencies, including dependencies for the server, use the command:

npm i

To run the application in development mode, use the command:

npm start

and for the final build, with all the minimization of the application in production mode, the command:

npm run build

This is a regular project using the Gulp task manager and the Webpack module builder. The server code is located in the app.js file, and the server itself is written using the Express framework.

The server file is a typical application on the Express framework:

const express = require('express')
const jsdom = require('jsdom')
const { JSDOM } = require('jsdom')
const fs = require('fs')
const port = process.env.PORT || 3000

// connect database file
let DB = JSON.parse(fs.readFileSync(__dirname + '/db.json').toString())

// create an Express application object
const app = express()

// create a parser for application/x-www-form-urlencoded data
const urlencodedParser = express.urlencoded({ extended: false })

// define directory for static files and ignore index.html file
app.use(express.static(__dirname + '/public', { index: false }))

// define an array of bot names that will receive the rendered content
const listBots = [
  'Yandex', 'YaDirectFetcher', 'Google', 'Yahoo', 'Mail.RU_Bot', 'bingbot', 'Accoona', 'Lighthouse',
  'ia_archiver', 'Ask Jeeves', 'OmniExplorer_Bot', 'W3C_Validator', 'WebAlta', 'Ezooms', 'Tourlentabot', 'MJ12bot',
  'AhrefsBot', 'SearchBot', 'SiteStatus', 'Nigma.ru', 'Baiduspider', 'Statsbot', 'SISTRIX', 'AcoonBot', 'findlinks',
  'proximic', 'OpenindexSpider', 'statdom.ru', 'Exabot', 'Spider', 'SeznamBot', 'oBot', 'C-T bot', 'Updownerbot',
  'Snoopy', 'heritrix', 'Yeti', 'DomainVader', 'DCPbot', 'PaperLiBot', 'StackRambler', 'msnbot'
]

// loads only scripts and ignores all other resources
class CustomResourceLoader extends jsdom.ResourceLoader {
  fetch(url, options) {
    return regJSFile.test(url) ? super.fetch(url, options) : null
  }
}

// define the bot agent string to test
const testAgent = process.argv[2] === 'test' ? 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' : ''

// define a regular expression to search for bot names in a string
const regBots = new RegExp(`(${listBots.join(')|(')})`, 'i')

// search for script file extensions
const regJSFile = /\.m?js$/

// favicon request
app.get('/favicon.ico', (req, res) => res.sendStatus(204))

// database request
app.get('/db', (req, res) => res.send(DB[req.query.page]))

// all other requests
app.use(async (req, res) => {
  // if the request comes from a bot
  if (regBots.test(testAgent || req.get('User-Agent'))) {
    // define a new JSDOM object with parameters
    const dom = await JSDOM.fromFile('index.html', {
      url: req.protocol + '://' + req.get('host') + req.originalUrl, // determine the full URL
      resources: new CustomResourceLoader(), // loading only scripts
      runScripts: 'dangerously' // allow page scripts to execute
    })

    // return the rendered HTML content of the page
    dom.window.onload = async () => res.send(await dom.window._$RtnRender_())
  }
  // otherwise, if the request comes from a user
  else {
    // return the main page file of the application
    res.sendFile(__dirname + '/index.html')
  }
})

// start the server
app.listen(port, () => console.log(`The server is running at http://localhost:${port}/`))

In order for the render() method to work on the server, jsdom is used – this is an implementation of Web standards in JavaScript.

Regular users do not need to give the rendered page content. It is only necessary for search bots and other automatic accounting systems for HTML content analysis. The list of these systems is in the array, which can be supplemented with additional names:

// define an array of bot names that will receive the rendered content
const listBots = [
  'Yandex', 'YaDirectFetcher', 'Google', 'Yahoo', 'Mail.RU_Bot', 'bingbot', 'Accoona', 'Lighthouse',
  'ia_archiver', 'Ask Jeeves', 'OmniExplorer_Bot', 'W3C_Validator', 'WebAlta', 'Ezooms', 'Tourlentabot', 'MJ12bot',
  'AhrefsBot', 'SearchBot', 'SiteStatus', 'Nigma.ru', 'Baiduspider', 'Statsbot', 'SISTRIX', 'AcoonBot', 'findlinks',
  'proximic', 'OpenindexSpider', 'statdom.ru', 'Exabot', 'Spider', 'SeznamBot', 'oBot', 'C-T bot', 'Updownerbot',
  'Snoopy', 'heritrix', 'Yeti', 'DomainVader', 'DCPbot', 'PaperLiBot', 'StackRambler', 'msnbot'
]

If any of these names are present in the request header, the server will return the rendered HTML content:

// if the request comes from a bot
if (regBots.test(testAgent || req.get('User-Agent'))) {
  // define a new JSDOM object with parameters
  const dom = await JSDOM.fromFile('index.html', {
    url: req.protocol + '://' + req.get('host') + req.originalUrl, // determine the full URL
    resources: new CustomResourceLoader(), // loading only scripts
    runScripts: 'dangerously' // allow page scripts to execute
  })

  // return the rendered HTML content of the page
  dom.window.onload = async () => res.send(await dom.window._$RtnRender_())
}

For all other requests, the server will return the index.html file, which is the only html file in this single page application:

// otherwise, if the request comes from a user
else {
  // return the main page file of the application
  res.sendFile(__dirname + '/index.html')
}

Rendering is performed using the _$RtnRender_() function of the global window object, as shown below:

// return the rendered HTML content of the page
dom.window.onload = async () => res.send(await dom.window._$RtnRender_())

This function is assigned to an object in the index.js file, which is the main file of the entire application:

// add the Render method as a property of the window object
window._$RtnRender_ = Render

Rendering does not support dynamic imports instead, you must use regular module import and export statements. Additionally, rendering does not support the global fetch() method. You must use the built-in XMLHttpRequest object instead.

The XMLHttpRequest object can be wrapped in a function and then called this function instead of writing the request code for this object manually each time, as shown in the file helpers.js, for example:

export const httpRequest = (url, method = 'GET', type = 'json') => {
  const xhr = new XMLHttpRequest()
  xhr.open(method, url)
  xhr.responseType = type
  xhr.send()
  return new Promise(ok => xhr.onload = () => ok(xhr.response))
}

After installing all the dependencies of the application from the package.json file, to start the server in normal mode, you need to open the console from the /server directory, and enter the following command in it:

node app

and to see how the server renders the content for search engines, you need to enter the command:

node app test
4.0.5

8 months ago

4.0.7

8 months ago

4.0.6

8 months ago

4.0.1

8 months ago

4.0.0

8 months ago

4.0.3

8 months ago

4.0.2

8 months ago

4.0.9

8 months ago

4.0.8

8 months ago

4.0.19

7 months ago

4.0.21

7 months ago

4.0.20

7 months ago

4.0.27

7 months ago

4.0.26

7 months ago

4.0.23

7 months ago

4.0.22

7 months ago

4.0.25

7 months ago

4.0.24

7 months ago

4.0.10

8 months ago

4.0.16

7 months ago

4.0.15

7 months ago

4.0.18

7 months ago

4.0.17

7 months ago

4.0.12

7 months ago

4.0.11

8 months ago

4.0.14

7 months ago

3.4.20

2 years ago

3.4.21

2 years ago

3.4.22

2 years ago

3.4.14

2 years ago

3.4.15

2 years ago

3.4.16

2 years ago

3.4.17

2 years ago

3.4.18

2 years ago

3.4.4

2 years ago

3.4.19

2 years ago

3.4.3

2 years ago

3.4.2

2 years ago

3.4.1

2 years ago

3.4.10

2 years ago

3.4.11

2 years ago

3.4.12

2 years ago

3.4.8

2 years ago

3.4.7

2 years ago

3.4.6

2 years ago

3.4.5

2 years ago

3.4.9

2 years ago

3.4.0

2 years ago

3.2.2

2 years ago

3.2.1

2 years ago

3.2.0

2 years ago

3.2.6

2 years ago

3.2.5

2 years ago

3.2.4

2 years ago

3.2.3

2 years ago

3.2.9

2 years ago

3.2.8

2 years ago

3.2.7

2 years ago

3.3.1

2 years ago

3.3.0

2 years ago

3.0.1

2 years ago

3.0.0

2 years ago

3.1.3

2 years ago

3.1.2

2 years ago

3.1.1

2 years ago

3.1.0

2 years ago

3.1.7

2 years ago

3.1.6

2 years ago

3.1.5

2 years ago

3.1.4

2 years ago

2.2.1

2 years ago

2.2.0

2 years ago

2.4.1

2 years ago

2.2.3

2 years ago

2.4.0

2 years ago

2.2.2

2 years ago

2.4.3

2 years ago

2.2.5

2 years ago

2.4.2

2 years ago

2.2.4

2 years ago

2.4.5

2 years ago

2.4.4

2 years ago

2.2.6

2 years ago

2.3.0

2 years ago

2.1.2

2 years ago

2.1.1

2 years ago

2.3.2

2 years ago

2.3.1

2 years ago

2.1.3

2 years ago

2.3.3

2 years ago

2.4.10

2 years ago

2.4.12

2 years ago

2.4.11

2 years ago

2.1.0

2 years ago

2.4.7

2 years ago

2.4.6

2 years ago

2.4.9

2 years ago

2.4.8

2 years ago

1.2.10

3 years ago

1.2.8

3 years ago

1.2.9

3 years ago

1.2.0

3 years ago

1.2.7

3 years ago

1.2.6

3 years ago

1.2.5

3 years ago

1.2.4

3 years ago

1.2.3

3 years ago

1.2.2

3 years ago

1.2.1

3 years ago

1.1.12

3 years ago

1.1.11

3 years ago

1.1.10

3 years ago

1.1.9

3 years ago

1.1.8

3 years ago

1.1.7

3 years ago

1.1.6

3 years ago

1.1.5

3 years ago

1.1.4

3 years ago

1.1.3

3 years ago

1.1.2

3 years ago

1.1.1

3 years ago

1.1.0

3 years ago

1.0.14

3 years ago

1.0.13

3 years ago

1.0.12

3 years ago

1.0.11

3 years ago

1.0.10

3 years ago

1.0.9

3 years ago

1.0.8

3 years ago

1.0.7

3 years ago

1.0.6

3 years ago

1.0.5

3 years ago

1.0.4

3 years ago

1.0.3

3 years ago

1.0.2

3 years ago

1.0.1

3 years ago

1.0.0

3 years ago