Advanced Web Components
npm install creaton-jsGitHub | GitFlic | GitVerse | NpmJS | Download⤵️
Creaton (short Ctn) is a JavaScript framework for quickly creating 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:
``js
class WHello {
// initializing the properties of a state object
message = 'Creaton'
color = 'orange'
static mode = 'open' // add Shadow DOM
// return the HTML markup of the component
static template() {
return
}
}
`
1. Quick start
2. Component state
3. Cycles
4. Mixins
5. Static properties
6. Special methods
7. Event Emitter
8. Router
9. Server-side rendering
Quick start
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 ctn.global.js file into this directory.
Add an index.html file to the directory with the following content:
`html
Document
`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:
Hello, Creaton!
The components can be placed in separate modules. In this case, the Hello component file would look like the following:
`js
export default class WHello {
// initializing the properties of a state object
message = 'Creaton'
color = 'orange' static mode = 'open' // add Shadow DOM
// return the HTML markup of the component
static template() {
return
}
}
`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:
`html
Hello, ${ this.message }!
`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:
`html
`
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:
`html
Document
Hello, ${ this.message }!
`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:
`html
`Regardless of the component type, when using the backquote character «`» in HTML markup, it must be escaped with the backslash character «\», as shown below:
`js
// return the HTML markup of the component
static template() {
return Hello\, ${ this.message }!
}
`This is due to the fact that the HTML markup of any component is always placed inside a template string. For single-file and embedded components, this is done at the level of converting them into a regular component class.
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:
`html
`Now open the browser console and enter the commands sequentially:
`
hello.$state.message = 'Web Components'
hello.$state.color = 'green'
hello.$update()
`The color and content of the header will change:
Hello, Web Components!
Component state
Each component can contain changing data, which is called a state. The state can be defined in the constructor of the component class:
`js
class WHello {
constructor() {
// initializing the properties of a state object
this.message = 'Creaton'
this.color = 'orange'
}
...
}
`Alternatively, using the new syntax, you can define the state directly in the class itself:
`js
class WHello {
// initializing the properties of a state object
message = 'Creaton'
color = 'orange'
...
}
`
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:
`js
class WHello {
// initializing the property of a state object
message = 'Creaton' // define the method of the state object
printStr(str) {
return this.message
}
// return the HTML markup of the component
static template() {
return
}
}
`
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'
`To update the component content based on the new state, the special method $update() is used, for example:
`
hello.$update()
`
When the content of a component is updated, its old DOM is not deleted. Instead, a temporary virtual DOM is created based on the returned contents of the static template() method and the updated state object. 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 is updated. Because the update will only change the old value of its attribute and text content:
`js
class WHello {
// initializing the property of a state object
message = 'Creaton' /* 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() {
return
}
}
`
Cycles
To output loops, the map() and join() methods or the reduce() method are used.
When using the map() method, you must add the join() method with an empty string argument at the end to remove commas between array elements:
`js
class WHello {
// initializing the property of a state object
rgb = ['Red', 'Green', 'Blue'] // return the HTML markup of the component
static template() {
return
- ${ col }
).join('') }
}
}
`When using the reduce() method, you do not need to add the join() method at the end:
`js
class WHello {
// initializing the property of a state object
rgb = ['Red', 'Green', 'Blue'] // return the HTML markup of the component
static template() {
return
- ${ col }
, '') }
}
}
`
You can use the special tagged function $tag, which automatically adds the join() method to all arrays and can call methods of the state object when they are specified in HTML markup without parentheses "()", for example:
`js
class WHello {
// initializing the properties of a state object
rgb = ['Red', 'Green', 'Blue']
message = 'Creaton' // define the method of the state object
printStr(str) {
return this.message
}
// return the HTML markup of the component
static template() {
return this.$tag
- ${ col }
) }
}
}
`By default, all embedded and single-file components use this function to create their HTML content:
`html
${ this.printArr }
`
To output objects, the Object.keys() method is used, as shown below:
`js
class WHello {
// initializing the property of a state object
user = {
name: 'John',
age: 36
} // define the method of the state object
printObj() {
const { user } = this
return Object.keys(user).map(prop =>
)
} // return the HTML markup of the component
static template() {
return this.$tag
}
}
`or a for-in loop, for example:
`js
// define the method of the state object
printObj() {
const { user } = this
let str = ''
for (const prop in user) {
str +=
}
return str
}
`
Mixins
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:
`html
Document
`
Static properties
name – this static property used, for example, when an anonymous class is passed to the Ctn function, as shown below:
`js
// pass the anonymous class of the Hello component to the Ctn function
Ctn(class {
static name = 'w-hello' // name of the component
// initializing the property of a state object
message = 'Creaton' // return the HTML markup of the component
static template() {
return
}
})
`
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:
`js
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 = 'Creaton'
// return the HTML markup of the component
static template() {
return
}
}
`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:
`html
`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 method returns the future HTML content of the component:
`js
// return the HTML markup of the component
static template() {
return
}
`
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:
`js
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 = 'Creaton'
} // return the HTML markup of the component
static template() {
return
}
}
`or get data from the server to initialize their. But in this case, the method must be asynchronous:
`js
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('Creaton'), 1000))
} // return the HTML markup of the component
static template() {
return
}
}
`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:
`js
class WHello {
// 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 = 'Creaton'
// return the HTML markup of the component
static template() {
return
}
}
`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:
`html
`
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:
`html
`
Special methods
All special methods and properties start with the dollar symbol «$» followed by the name of the method or property.
\$update() – this special method is performed to update the contents of the component after its state has changed:
`
hello.$state.message = 'Web Components'
hello.$update()
`This method updates the contents of private components only if it is called from static methods of the component class. For all other component types, it returns the number of milliseconds it took for the component's contents to be updated.
\$() – this special method selects an element from the component content by the specified selector, for example, to add an event handler to the element:
`js
// 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:
`js
// 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:
`js
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('
`
\$tag() – this special tagged function, which automatically adds the join() method to all arrays and can call methods of the state object when they are specified in HTML markup without parentheses "()", for example:
`js
class WHello {
// initializing the properties of a state object
rgb = ['Red', 'Green', 'Blue']
message = 'Creaton'
// define the method of the state object
printStr(str) {
return this.message
}
// return the HTML markup of the component
static template() {
return this.$tag
- ${ col }
) }
}
}
`
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:
`js`
import Ctn, { Tag, Event, Router, Render } from "./ctn.esm.js"
The Ctn 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:
` html`
\$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:
` html`
\$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:
` html`
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 Ctn function.
If the method is called as a constructor, it returns a new emitter object that will generate and track user events, for example:
`js`
const emit = new Ctn.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:
`js`
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:
`js`
emit.addEventListener('new-array', event => {
this.rgb = event.detail
this.$update()
})
In the webpack build system, the emitter can be exported from a separate module, for example, from a file Events.js:
`js`
import { Event } from 'creaton-js'
export const Emit = new Event()
for the subsequent import of the emitter in the files of the components that will use it:
`js`
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:
` html`
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 Ctn 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:
`js`
const emitRouter = new Ctn.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:
`js`
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:
`js`
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:
`js`
emitRouter.addEventListener('/about', event => {
const arr = event.detail
...
})
The initial slash «/» in the name of the route event is optional:
`js`
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:
`html`
About
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:
`js`
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:
`js/abou\w
emitRouter.addEventListener(String.raw, event => {`
...
})
or so:
`js/abou\w
const raw = String.raw
emitRouter.addEventListener(raw, event => {`
...
})
In addition to the detail property, the Event object has an additional params property to get route parameters, as shown below:
`js`
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:
`html`
Product
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:
`js/categories\?catId=\d&prodId=\d
const raw = String.raw
emitRouter.addEventListener(raw, event => {`
const catId = event.search.get("catId")
const prodId = event.search.get("prodId")
...
})
This handler will be executed for all links of the form:
`html`
Product
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:
`js`
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:
` html`
To handle the routes of these pages, the router emitter is assigned a handler with an optional route parameter in the Content component:
`js(:page)?
// add an event handler to the emitter with an optional route parameter
emitRouter.addEventListener(, event => {w-${event.params.page || 'home'}
// assign a page component name to the property
this.page = `
this.$update() // update component
})
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:
`js`
// 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:
`js`
// 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)
})
To prevent a page component with an undefined name from being created when the application is opened, a conditional check is used on the value of the page property of the Content component's state object:
`js<${this.page} />
// return the HTML markup of the component
static template() {
// if the property contains the page name
if (this.page) {
return `
}
}
This example uses a self-closing tag for the component element to be connected:
`js<${this.page} />`
If the plug-in component contained slots into which some HTML content would be passed, then it would be necessary to use the opening and closing tags of the component element:
`js
// return the HTML markup of the component
static template() {
// if the property contains the page name
if (this.page) {
return
<${this.page}>
${this.page}>
`
}
}
When the static template() method returns nothing, a component with empty HTML content is created.
You can only pass HTML content to slots for components that have Shadow DOM. This means that when updating a component, HTML content passed from a component without Shadow DOM is simply ignored and no changes are made to it.
To pass data to any components, you can use custom attributes, for example:
`js<${this.page} data-color="${this.color}" />
// return the HTML markup of the component
static template() {
// if the property contains the page name
if (this.page) {
// pass the color value through the custom "data-color" attribute
return `
}
}
Unlike HTML content, the attributes of any component's element are updated always.
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 Ctn 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:
` html
Advanced Web Components