// optional: import observable data type
import { ObservableVariable, ObservableArray, ObservableMap, ObservableLocalStorageVariable, createObservable, checkIfObservable } from '@lexriver/dome'
// more details: https://www.npmjs.com/package/@lexriver/observable
// optional: import TypeEvent
import { TypeEvent } from '@lexriver/dome'
// more details: https://www.npmjs.com/package/@lexriver/type-event
// optional: import data types checker
import {DataTypes} from '@lexriver/dome'
// more details: https://www.npmjs.com/package/@lexriver/data-types
// optional: import Async methods
import {Async} from '@lexriver/dome'
// more details: https://www.npmjs.com/package/@lexriver/async
`
Creating component
`tsx
interface Attrs{
counter:number
}
export class MyComponent extends DomeComponent{
render(){
return
counter={this.attrs.counter}
}
}
`
or functional component
`tsx
const MyComponent = (attrs, children) =>
counter={attrs.counter}
`
Mount component to DOM
`typescript
document.body.appendChild()
`
Example of component with dynamic content load
`tsx
interface Attrs{
text:string
}
export class MyComponent extends DomeComponent{
protected refContainer!:HTMLDivElement // for reference to main div of component
render(){
return
this.refContainer = ref}>loading...
}
async afterRender(){
this.scheduleUpdate() // update component right after the first render
}
async updateAsync(){
let remoteText = await fetch(...) // get remoteText from server
DomeManipulator.replaceAllChildrenAsync(this.refContainer, <>text={remoteText}>) // replace 'loading...' with text
}
}
`
Special property this.rootElement can be used for reference to main element.
`tsx
export class MyComponent extends DomeComponent{
render(){
// div is rootElement for this component
return
loading...
}
async afterRender(){
this.scheduleUpdate()
}
async updateAsync(){
let remoteText = await fetch(...) // get remoteText from server
DomeManipulator.replaceAllChildrenAsync(this.rootElement, <>text={remoteText}>) // replace 'loading...' with text
}
}
`
Attributes (properties) for component are not read only, so we can change them, but this.scheduleUpdate() should be called to re-render the component.
Keep in mind that if parent component will be re-rendered then child component will be re-rendered also with attributes used in parent component.
`tsx
interface Attrs{
text?:string
}
export class MyComponent extends DomeComponent{
render(){
if(!this.attrs.text){
return
loading...
}
return
text={this.attrs.text}
}
async afterRender(){
this.attrs.text = await fetch(...)
this.scheduleUpdate() // scheduleUpdate executes updateAsync() method if it is defined else render() method
}
}
`
There is no such thing as State of component.
It's recommended to use Observable attributes or the global state for application as a colleciton of observable variables.
Example of Observable attributes
Every time the observable attribute changes then the updateAsync() or render() method will be executed.
`tsx
interface Attrs{
textO:ObservableVariable }
export class MyComponent extends DomeComponent{
render(){
return
text={this.attrs.textO.get()}
}
async afterRender(){
this.attrs.textO.set(await fetch(...)) // when fetch completes the component will be updated
}
}
let myObservableStringO = new ObservableVariable('default value')
setTimeout(() => {
myObservableStrginO.set('another value')
}, 3000)
document.body.appendChild()
`
For more details on Observable please visit: https://github.com/LexRiver/observable
Example of auto-unsubscribe component from some update events on component unmount from DOM
`tsx
export module GlobalState{
export const someStringO = new ObservableVariable('default text')
export const someEvent = new TypeEvent<(counter:number)=>void>()
}
interface Attrs{
}
export class MyComponent extends DomeComponent{
render(){
return
text={GlobalState.someStringO.get()}
}
async afterRender(){
GlobalState.someStringO.eventOnChange.subscribe((newValue) => {
if(DomeManipulator.isInDom(this.rootElement) == false) return {unsubscribe:true} // remove this listener when component unmounts from DOM
this.scheduleUpdate() // update component when someStringO changes
})
GlobalState.someEvent.subscribe((newValue:number) => {
if(DomeManipulator.isInDom(this.rootElement) == false) return {unsubscribe:true} // remove this listener when component unmounts from DOM
console.log('event in component')
this.scheduleUpdate() // update component when someEvent triggers
})
}
}
document.body.appendChild()
setTimeout(() => {
GlobalState.someStringO.set('another text') // change the value forces the component to update
}, 3000)
setTimeout(() => {
GlobalState.someEvent.triggerAsync(100) // trigger the event forces the component to update
}, 5000)
setTimeout(() => {
DomeManipulator.removeAllChildrenAsync(document.body) // remove component from DOM forces to unsubscribe from events, so the next event will not be triggered
}, 7000)
setTimeout(() => {
GlobalState.someEvent.triggerAsync(100) // trigger the event, but no listeners
}, 10000)
`
There are also features for animation, dynamically change css classes and router, please keep reading.
node-sass -- provides binding for Node.js to LibSass, a Sass compiler.
sass-loader -- is a loader for Webpack for compiling SCSS/Sass files.
style-loader -- injects our styles into our DOM.
css-loader -- interprets @import and @url() and resolves them.
mini-css-extract-plugin -- extracts our CSS out of the JavaScript bundle into a separate file, essential for production builds.
*/
module.exports = {
mode: 'development',
entry: [
'webpack-dev-server/client?http://localhost:8181',// bundle the client for webpack-dev-server and connect to the provided endpoint
'webpack/hot/only-dev-server', // bundle the client for hot reloading, only- means to only hot reload for successful updates
'regenerator-runtime/runtime',
'./src/website/App.tsx' // the entry point of our app
],
output: {
path: resolve(__dirname, './webpack-out'), //The output directory as an absolute path.
publicPath: '/', //https://webpack.js.org/configuration/output/#outputpublicpath
filename: '[name].[hash].bundle.js' //This option determines the name of each output bundle. The bundle is written to the directory specified by the output.path option.
},
devServer: {
hot: true, // enable HMR on the server
port: 8181,
historyApiFallback:true
},
new MiniCssExtractPlugin({
filename: 'css/[contenthash].css',
chunkFilename: 'css/[id].[contenthash].css',
//filename: 'css/[name].[hash].css',
//chunkFilename: 'css/[id].[hash].css'
}),
new CopyPlugin([
//{from: resolve(__dirname, './webpack-src/*.png'), to: resolve(__dirname, './webpack-out/')},
//{from: resolve(__dirname, './webpack-src/*.ico'), to: resolve(__dirname, './webpack-out/')},
{from: './webpack-src/site.webmanifest', flatten:true},
{from: './webpack-src/*.png', flatten: true},
{from: './webpack-src/favicon.ico'},
//{from: './webpack-src/*.webmanifest'}
])
],
}
`
Your tsconfig.json should be like this:
`javascript
"compilerOptions": {
/ Basic Options /
"target": "esnext",
"moduleResolution": "node",
"module": "esnext",
"jsx": "react",
"declaration": true, / Generates corresponding '.d.ts' file. /
// "declarationMap": true, / Generates a sourcemap for each corresponding '.d.ts' file. /
"sourceMap": true, / Generates corresponding '.map' file. /
// "outFile": "./", / Concatenate and emit output to single file. /
"outDir": "./out", / Redirect output structure to the directory. /
//"rootDir": "./", / Specify the root directory of input files. Use to control the output directory structure with --outDir. /
//"rootDir": "./src",
// "composite": true, / Enable project compilation /
// "removeComments": true, / Do not emit comments to output. /
// "noEmit": true, / Do not emit outputs. /
// "importHelpers": true, / Import emit helpers from 'tslib'. /
// "downlevelIteration": true, / Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. /
// "isolatedModules": true, / Transpile each file as a separate module (similar to 'ts.transpileModule'). /
/ Strict Type-Checking Options /
//"strict": false,
"strict": true, / Enable all strict type-checking options. /
"noImplicitAny": false, / Raise error on expressions and declarations with an implied 'any' type. /
// "strictNullChecks": true, / Enable strict null checks. /
// "strictFunctionTypes": true, / Enable strict checking of function types. /
// "strictPropertyInitialization": true, / Enable strict checking of property initialization in classes. /
// "noImplicitThis": true, / Raise error on 'this' expressions with an implied 'any' type. /
// "alwaysStrict": true, / Parse in strict mode and emit "use strict" for each source file. /
/ Additional Checks /
// "noUnusedLocals": true, / Report errors on unused locals. /
// "noUnusedParameters": true, / Report errors on unused parameters. /
// "noImplicitReturns": true, / Report error when not all code paths in function return a value. /
// "noFallthroughCasesInSwitch": true, / Report errors for fallthrough cases in switch statement. /
/ Module Resolution Options /
// "moduleResolution": "node", / Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). /
// "baseUrl": "./", / Base directory to resolve non-absolute module names. /
// "paths": {}, / A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. /
// "rootDirs": [], / List of root folders whose combined content represents the structure of the project at runtime. /
// "typeRoots": [], / List of folders to include type definitions from. /
// "types": [], / Type declaration files to be included in compilation. /
// "allowSyntheticDefaultImports": true, / Allow default imports from modules with no default export. This does not affect code emit, just typechecking. /
"esModuleInterop": true, / Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. /
// "preserveSymlinks": true, / Do not resolve the real path of symlinks. /
/ Source Map Options /
// "sourceRoot": "", / Specify the location where debugger should locate TypeScript files instead of source locations. /
// "mapRoot": "", / Specify the location where debugger should locate map files instead of generated locations. /
// "inlineSourceMap": true, / Emit a single file with source maps instead of having a separate file. /
// "inlineSources": true, / Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. /
/ Experimental Options /
"experimentalDecorators": true, / Enables experimental support for ES7 decorators. /
"emitDecoratorMetadata": true, / Enables experimental support for emitting type metadata for decorators. /
}
}
`
add babel.config.js to your root:
`javascript
// use babel.config.js to apply to imported packages also
console.log('=== loading babel.config.js')
module.exports = {
"presets": [
[
"@babel/env",{
//"modules" : false, //By setting modules to false, we are telling babel not to compile our module code. This will lead to babel preserving our existing es2015 import/export statements.
"targets": {
"browsers": [
"cover 99.5%"
]
}
}
],
add these scripts to your package.json:
`json
"scripts": {
"build": "npm run clean-webpack-out && webpack -p --config=webpack.config.prod.js",
"clean-webpack-out": "rimraf webpack-out/*",
"lint": "tslint './src/*/.ts*' --format stylish --project . --force",
"start": "npm run start-dev",
"start-dev": "webpack-dev-server --config=webpack.config.dev.js"
}
`
So when you run
` npm run start
` the development process will be started on localhost:8181
and when your run
` npm run build
` the production website will be generated in ./webpack-out
API
DomeComponent
Create a custom component
`tsx
interface Attrs{ // to add custom attributes to your component
id?:number
}
export class MyComponent extends DomeComponent{
render(){
return
id={this.attrs.id}, children={this.children}
}
}
`
Then use it like this
`tsx
Text inside `
There are also internal attributes for any component:
* ref?:(ref)=>void - to take a reference to this component
* onShowAnimation?:Animation - animation for show element
* onHideAnimation?:Animation - animation for hide element
And Animation type is
`typescript
export interface Animation{
cssClassName:string
timeMs:number // time in milliseconds before removing cssClassName from element
}
`
Use these attributes like so:
`tsx
myRef = ref} onShowAnimation={{cssClassName:css.animationShow, timeMs:300}} onHideAnimation={myHideAnimationObject} />
`
Style html elements
Inline styles:
`tsx
`
Css classes could be set by using class attribute or by aliases className and cssClasses
`tsx
//same
//same
`
Instead of string CssClass type can be used, for example:
`tsx
`
Please see DomeManipulator.setCssClasses(..) for more details.
Events for html elements
To create an event use standart event names but in camel case:
`tsx
`
Set inner html
To set inner html for element:
`tsx
hi} />
`
Properties for DomeComponent
rootElement:Element|HTMLElement
This is a reference to root element like that was used in first render.
Do not use it with fragment <>> as a root element.
attrs
Contains all attributes for component including internal attributes (see above)
children
Contains all children inside component
Methods for custom component
init()
This method is for overwrite. It will be executed before first render.
`tsx
interface Attrs{ // to add custom attributes to component
id?:number
}
export class MyComponent extends DomeComponent{
render(){
return
id={this.attrs.id}, children={this.children}
}
init(){
console.log('init!')
}
}
`
render()
This method must be overwritten. It will be executed when component first rendered and also when updated if updateAsync() was not overwritten.
This method must return DOM element or elements.
To return a few elements fragment syntax <>> can be used.
`tsx
interface Attrs{
id?:number
}
export class MyComponent extends DomeComponent{
render(){
return <>
id={this.attrs.id}
children={this.children}
>
}
}
`
afterRender()
This method will be executed after first render.
`tsx
interface Attrs{
id?:number
}
export class MyComponent extends DomeComponent{
render(){
return
id={this.attrs.id}
}
afterRender(){
console.log('after render')
}
}
`
updateAsync()
This method can be overwritten. By default this method will call render() method to update the component. And after that afterUpdate() will be executed.
Use method scheduleupdate() to force component to re-render.
`tsx
interface Attrs{
id?:number
}
export class MyComponent extends DomeComponent{
render(){
return
Use this method to force update the component. This method is not for overwrite.
It should be used inside component:
`tsx
this.scheduleUpdate()
`
afterUpdate()
This method will be executed after component update, but not after first render.
Overwrite this method to take effect.
Animation
To add an animation for render or update component use attributes onShowAnimation and onHideAnimation.
These attributes uses an Animation type:
`typescript
export interface Animation{
cssClassName:string // the name of css class to be applied to DOM element
timeMs:number // amount of milliseconds to wait before removing cssClassName from element
}
`
Wait for document.body.clientHeight or document.body.clientWidth to be enough to scroll to pxFromTop or pxFromLeft and then scroll to that position.
Parameters
* pxFromTop?:number : amount of pixels from top to scroll to
* pxFromLeft?:number : amount of pixels from left to scroll to
* smooth?:boolean : true for smooth scroll. Default value is false.
* msStep?:number : check if scroll is possible at each milliseconds step. Default value is 50 * maxMsToWait?:number : max amount of milliseconds to wait for to be able to scroll. Default value is 5000
Add reaction on route change.
Parameters:
* route:string must starts with '/', for example '/login' or '/'. To add a parameter add : before parameter name: /product/:id * exactMatch:boolean if true then will be triggered only if whole route matches the pattern
* action:RouteAction is a method to execute if route matches
Where RouteAction type is:
`typescript
export type RouteAction = (
params:{[key:string]:string}, // parameters from url
url:string,
scrollToPreviousPositionAsync:()=>Promise // this function can be called to scroll to previous page position
) => void|Promise `
example:
`typescript
DomeRouter.onRoute('/login', true, (params, url) => changePage())
DomeRouter.onRoute('/product/:productId', true, (params, url) => {
// for example for '/product/456' the output will be
// 'productId=', '456', string
console.log('productId=', params.productId, typeof params.productId)
})
`
A type of parameter can be added, for example :
`typescript
DomeRouter.onRoute('/product/:productId', true, (params, url) => {
// for example for '/product/456' the output will be
// 'productId=', 456, number
console.log('productId=', params.productId, typeof params.productId)
})
`
A possible types are:
* int - uses parseInt(p) internally
* float - uses parseFloat(p) internally
* number - uses Number(p) intrenally
But there is no validation for paramters, so the result of specified function will be returned.
`typescript
DomeRouter.onRoute('/product/:productId', true, (params, url) => {
// for example for '/product/456' the output will be
// 'productId=', 456, number
console.log('productId=', params.productId, typeof params.productId)
})
`