@innet/dom
Abstract
This is aninnet
tool, that helps to create frontend-side application.Here you can find JSX components, state-management, portals, context, slots, routing and more.
Based on innet.
Install
Use innetjs to startinnet-dom
app development.npx innetjs init my-app -t fe
change my-app to work folder nameGo into
my-app
and check README.md
Handler
Usedom
handler to start an application.Clear
src
folder and create index.ts
inside.
import innet from 'innet'
import dom from '@innet/dom'
import app from './app'
innet(app, dom)
JSX
You can use xml-like syntax to create and append elements into the DOM. More information about JSX here.Create
app.tsx
in src
folder.
```typescript jsx
export default (
Hello World!
Everything, that you provide as the first argument of `innet` function with the `dom` handler,
will fall into the `body` DOM-element.
## portal
If you want to put your content into another element (not `body`), use portal element.
For example, you can change `index.html` from `public` folder.
```html
<!doctype html>
<html lang="en">
<head ... >
<body>
<div id="app"></div>
<!-- add this ^ -->
</body>
</html>
And change
app.tsx
```typescript jsx const app = document.getElementById('app')
export default (
<h1>
Hello World!
</h1>
)
You can use `portal` everywhere inside the app.
Change `app.tsx`
```typescript jsx
const app = document.getElementById('app')
const myElement = document.createElement('div')
export default (
<portal parent={app}>
<h1>
Hello World!
</h1>
<portal parent={myElement}>
This is content of myElement
</portal>
</portal>
)
myElement
should contain This is content of myElement
and app
should contain the next code.
<h1>
Hello World!
</h1>
State Management
Usually, state management is available only inside a component.With
innet
you can fully exclude component approach, but state management still to be available.The state management based on watch-state
To bind state and content, use
State
, Cache
or a function as the content.Turn back
index.html
and change app.tsx
```typescript jsx
import { State } from 'watch-state'const count = new State(0)
const increase = () => { count.value++ }
export default ( <>
<h1>
Count: {count}
</h1>
<button onclick={increase}>
Click Me
</button>
</>
)
To bind a state and a prop use `State`, `Cache` or a function as a value of the prop.
Change `app.tsx`
```typescript jsx
import { State } from 'watch-state'
const darkMode = new State(false)
const handleChange = (e: Event) => {
darkMode.value = e.target.checked
}
export default (
<div class={() => darkMode.value ? 'dark' : 'light'}>
<h1>
Hello World!
</h1>
<label>
<input
type="checkbox"
onchange={handleChange}
/>
Dark Mode
</label>
</div>
)
Components
Component is a function. You can use it as JSX element.Create
Content.tsx
```typescript jsx
export const Content = () => (
Hello World!
Change `app.tsx`
```typescript jsx
import { Content } from './Content'
export default (
<Content />
)
props
Any component gets an argumentprops
.If props have not provided the argument equals
undefined
else you get an object that contains the props.Change
Content.tsx
```typescript jsx
export function Content ({ color }) {
return (<h1 style={{ color }}>
Hello World!
</h1>
)
}
Then you should use the `color` prop outside.
Change `app.tsx`
```typescript jsx
import { Content } from './Content'
export default (
<Content color='red' />
)
Hooks
You can use hooks inside a component. Sync hooks should be used beforeawait
,
async hooks should be used as the first await
.```typescript jsx export async function Content (props1) { const sync1 = useSyncHook1() const sync2 = useSyncHook2()
const
async1,
async2,
= await Promise.all(useAsyncHook1(),
useAsyncHook2(),
)
// other }
#### useProps
You can get props with `useProps` hook.
```typescript jsx
import { useProps } from '@innet/jsx'
export function Content (props1) {
const props2 = useProps()
return (
<h1>
{props1 === props2 ? 'same' : 'different'}
</h1>
)
}
useChildren
To get children elements you can takeuseChildren
.Change
Content.tsx
```typescript jsx
import { useChildren } from '@innet/jsx'export function Content ({ color }) { const children = useChildren()
return (
<h1 style={{ color }}>
{children}
</h1>
)
}
Then you can use the children outside.
Change `app.tsx`
```typescript jsx
import { Content } from './Content'
export default (
<Content color='red'>
Hello World!
</Content>
)
Return
A component awaits a return:string
,number
- render as text node
-
null,
undefined,
boolean,
symbol` - ignore
```typescript jsx
const Test1 = () => null
const Test2 = () => {}
const Test3 = () => true
const Test4 = () => Symbol()
```- DOM Element - put in the DOM
- JSX Fragment,
array` - render content
```typescript jsx
const Test1 = () => <>content</>
const Test2 = () => 'content'
```- JSX Element - put in the DOM
content
const Test2 = () => ```
- JSX Plugin - run plugin
- function - observable children
Life Cycle
Each component renders only once!There are 3 steps of life cycle:
- render (DOM elements are not created)
- mounted (DOM elements are created)
- destroy (elements will be removed from the DOM)
Because of a component renders only once you can have effects right inside the component function. ```typescript jsx import { State } from 'watch-state'
function Content () { const state = new State()
fetch('...')
.then(e => e.json())
.then(data => {
state.value = data.text
})
return (<div>
{state}
</div>
)
}
### Async Component
Innet supports async components, you can simplify previous code.
```typescript jsx
async function Content () {
const { text } = await fetch('...').then(e => e.json())
return <div>{text}</div>
}
innetjs helps to make code splitting. ```typescript jsx async function Content () { const { Test } = await import('./Test')
return (
<div>
<Test />
</div>
)
}
`Test.tsx`
```typescript jsx
export const Test = () => (
<div>
Test success!
</div>
)
While it's loading nothing can be shown. If you want to show something, use
Generic Async Component
.Generic Async Component
Just add a star and useyield
instead of return
```typescript jsx
async function Content () {
yield 'Loading...'const { text } = await fetch('...').then(e => e.json())
yield
{text}
}
### Generic Component
It can be useful when you want to do something after a content deployed.
```typescript jsx
function * Content () {
yield (
<div id='test'>
Hello World!
</div>
)
colsole.log(document.getElementById('test'))
}
You can use
queueMicrotask
instead of a generic component, but there are a small difference:queueMicrotask
runs after whole content is available and generic component runs right after the content of the component rendered.
```typescript jsx
function A () {
queueMicrotask(() => {console.log(
'queueMicrotask A',
document.getElementById('a'),
document.getElementById('b'),
)
})yield
console.log(
'generic A',
document.getElementById('a'),
document.getElementById('b'),
)
}function B () { queueMicrotask(() => {
console.log(
'queueMicrotask B',
document.getElementById('a'),
document.getElementById('b'),
)
})yield
console.log(
'generic B',
document.getElementById('a'),
document.getElementById('b'),
)
}function Content () { return (
<>
<A />
<B />
</>
)
}
You get the next output:
generic A null
generic B
queueMicrotask A
queueMicrotask B
## Ref
`Ref` helps to get an HTML element.
```typescript jsx
import { Ref } from '@innet/dom'
function * Content () {
const wrapper = new Ref<HTMLDivElement>()
yield (
<div ref={wrapper}>
Hello World!
</div>
)
colsole.log(wrapper.value)
}
onDestroy
You can subscribe on destroy of a component byonDestroy
from watch-state
Change
Content.tsx
```typescript jsx import { State, onDestroy } from 'watch-state'
export function Content() { const count = new State(0) // create a state
const timer = setInterval(() => {
count.value++
}, 1000)
// increase the state each secondonDestroy(() => clearInterval(timer)) // stop timer on destroy
return () => count.value // return observable value }
And change `app.tsx`
```typescript jsx
import { State } from 'watch-state'
import { Content } from './Content'
const show = new State(true)
const handleChange = (e: Event) => {
show.value = e.target.checked
}
export default (
<>
<show when={show}>
<Content />
</show>
<input
type="checkbox"
checked
onchange={handleChange}
/>
</>
)
Context
You can pass a value from a parent element through any children to the place you need.Change
Content.tsx
```typescript jsx
import { Context, useContext } from '@innet/dom'export const color = new Context('blue')
export function Content () { const currentColor = useContext(color)
return (
<h1 style={{ color: currentColor }}>
{children}
</h1>
)
}
And change `app.tsx`
```typescript jsx
import { Content, color } from './Content'
export default (
<>
<Content>
Without context
</Content>
<context for={color} set='red'>
<Content>
With context
</Content>
</context>
</>
)
show
You can useshow
element to show/hide content by state.```typescript jsx import { State } from 'watch-state'
const show = new State(true)
export default (
<button
onclick={() => {
show.value = false
}}>
Click Me
</button>
)
> `when` can be: `State` | `Cache` | `() => any` | `any`
## hide
You can use `hide` element to show/hide content by state.
```typescript jsx
import { State } from 'watch-state'
const isHidden = new State(false)
export default (
<hide when={isHidden}>
<button
onclick={() => {
isHidden.value = true
}}>
Click Me
</button>
</hide>
)
when
can be:State
|Cache
|() => any
|any
switch
You can useswitch
element to show a content by string state.```typescript jsx import { State } from 'watch-state'
const str = new State('')
const case1 = () => { str.value = 'case1' }
const case2 = () => { str.value = 'case2' }
export default (
<slot name='case1'>
Case 1
<button
onclick={case2}>
Next
</button>
</slot>
<slot name='case2'>
Case 2
</slot>
Default content
<button
onclick={case1}>
Next
</button>
)
> `of` can be: `State<string | number>` | `Cache<string | number>` | `() => (string | number)` | `string | number`
## map
You can use `map` method of an array to put view on data.
```typescript jsx
const names = ['Mike', 'Alex', 'Dan']
export default (
<ul>
{names.map(name => (
<li>
{name}
</li>
))}
</ul>
)
It's ok for static data, but if you use a state, it's better to use
map
element.
```typescript jsx
import { State } from 'watch-state'
import { useMapValue, useMapIndex } from '@innet/dom'const names = new State('Mike', 'Alex', 'Dan')
function User () { const name = useMapValue() const index = useMapIndex()
return (
<li>
#{index}:
{name}
</li>
)
}export default (
<map of={names}>
<User />
</map>
)
Use `key` property to improve `DOM` changes when you use an array of objects with some uniq field, like id.
```typescript jsx
import { State } from 'watch-state'
const names = new State([
{ id: 1, text: 'test1' },
{ id: 2, text: 'test2' },
{ id: 3, text: 'test3' },
])
function User () {
const name = useMapValue()
const index = useMapIndex()
return (
<li>
#{index}:
{name}
</li>
)
}
export default (
<ul>
<map of={names} key='id'>
<User />
</map>
</ul>
)
slots
You can use slots to provide a couple of named child elements. ```typescript jsx import { useChildren } from '@innet/jsx'export const Content = () => (
<div class='header'>
<slot name='header'>
default header
</slot>
</div>
<div class='content'>
<slot>
default content
</slot>
</div>
<div class='footer'>
<slot name='footer'>
default footer
</slot>
</div>
)
```typescript jsx
export default (
<Content>
<slot>Custom content</slot>
<slot name='header'>
Custom header
</slot>
</Content>
)
You get
Custom header
, Custom content
and default footer
useSlots
useSlots
is a way to get slots.
```typescript jsx
import { useSlots } from '@innet/dom'export const Content = () => { const {
'': content,
header,
footer
} = useSlots()return (
<>
<show when={header}>
<div class='header'>
{header}
</div>
</show>
<div class='content'>
{content}
</div>
<show when={footer}>
<div class='footer'>
{footer}
</div>
</show>
</>
)
}
> Any slots without name or with name equals empty string and any content outside slots collect into empty string slot.
```typescript jsx
export default (
<Content>
<slot name='header'>
Custom header
</slot>
Custom content
</Content>
)
You can use a couple of slots with the same name. ```typescript jsx export default (
<slot name='header'>
Custom header1 <br />
</slot>
<slot name='header'>
Custom header2
</slot>
Custom content
)
## router
You can render content by url.
```typescript jsx
export const Content = () => (
<router>
<slot name='/'>
Home page
</slot>
<slot name='settings'>
Settings page
</slot>
Not Found
</router>
)
There are strong matching by default, so you can see
/
- Home page/settings
- Settings page/settings/test
- Not Found/any-other
- Not FoundIf you want to show
Settings page
on /settings/test
, use ish
prop on router element
```typescript jsx
export const Content = () => (
<slot name='/'>
Home page
</slot>
<slot name='settings'>
Settings page
</slot>
Not Found
)
When you use a router, that is inside a slot of another router, the route checks the next peace of url path.
```typescript jsx
export const Content = () => (
<router ish>
<slot name='/'>
Home page
</slot>
<slot name='settings'>
<router>
<slot name='main'>
Main Settings
</slot>
<slot name='user'>
User Settings
</slot>
Settings
</router>
</slot>
Not Found
</router>
)
/
- Home page/settings
- Settings/settings/main
- Main Settings/settings/user
- User Settings/settings/any-other
- Settings/any-other
- Not FoundYou can use
search
prop to make router binds on query search params
```typescript jsx
export const Content = () => (
<slot name='login'>
Login
</slot>
<slot name='logout'>
Logout
</slot>
)
`?modal=login` - Login
`/settings?modal=logout` - Logout
`/settings?user=1&modal=logout` - Logout
`/any-other?any-params&modal=any-other` - render nothing
## useRoute
You can handle dynamic routes by `useRoute`.
```typescript jsx
const Test = () => {
const route = useRoute()
return () => route.value
}
export const Content = () => (
<router ish>
<slot name='/'>
Home page: <Test />
</slot>
<slot name='settings'>
Settings: <Test />
</slot>
Other: <Test />
</router>
)
/
- Home page: //settings
- Settings: //settings/test
- Settings: test /any-other
- Other: any-othera
The taga
has a couple of features.rel="noopener noreferrer nofollow"
andtarget="_blank"
are default for external links.
href
Ifhref
starts from /
, ?
or #
then the Link will use History API.```typescript jsx export const Content = () => (
<a href="/">home</a>
<a href="/test">test</a>
<a href="/home">unknown</a>
<a href="?modal=test">modal</a>
<div>
<router>
<slot name='/'>
Home Page
</slot>
<slot name='test'>
Test Page
</slot>
404
</router>
<router search='modal'>
<slot name='test'>
Test Modal
</slot>
</router>
</div>
### replace
By default, it pushes to history, but you may use `replace` to replace current history state.
```typescript jsx
export const Content = () => (
<a replace href="/">
home
</a>
)
class
You can add root or active link class```typescript jsx const classes = { root: 'link', active: 'active', }
export const Content = () => (
<a
href="/"
class='only-root'>
home
</a>
<a
href="/test"
class={classes}>
test
</a>
You can use all features from [html-classes](https://www.npmjs.com/package/html-classes) for the `class` prop.
```typescript jsx
const classes = {
root: ['link1', 'link2', () => 'dynamic-class'],
active: { active: true },
}
export const Content = () => (
<div>
<a
href="/"
class={() => 'dynamic-root'}>
home
</a>
<a
href="/test"
class={classes}>
test
</a>
</div>
)
exact
By default, active class appends if URL starts withhref
prop value, but use exact
to compare exactly.```typescript jsx const classes = { root: 'link', active: 'active' }
export const Content = () => (
<a
href="/"
exact
classes={classes}>
home
</a>
<a
href="/test"
classes={classes}>
test
</a>
### scroll
You can use smooth scroll
```css
body, html {
scroll-behavior: smooth;
}
The property of scroll
says should we scroll on click and how.by default equals before
```typescript jsx export const Content = () => (
<a href="/" scroll='before'>
home
</a>
<a href="/test" scroll='after'>
test
</a>
<a href="?modal" scroll='none'>
test
</a>
### scrollTo
If you want to scroll the page to custom position (by default it's up of the page) use `scrollTo`
```typescript jsx
export const Content = () => (
<div>
<a href="/" scrollTo={100}>
home
</a>
<a href="/test" scrollTo='#root'>
test
</a>
</div>
)
Use a string to scroll under an element relates to the CSS selector you provide or use
-1
to stop scrolling.delay
You can show and hide elements with delay.```typescript jsx export function Content () { return (
<delay show={1000}>
Works
<delay show={1000}>
fine!
</delay>
</delay>
)
}
### useHidden
You can react on removing of elements
Change `Content.tsx`
```typescript jsx
import { useHidden } from '@innet/dom'
export function Content () {
const hidden = useHidden()
return () => hidden.value ? 'hidden' : 'shown'
}
And change
app.tsx
```typescript jsx
import { State } from 'watch-state'const show = new State(true)
const handleClick = () => { show.value = false }
export default () => show.value && (
<Content />
<button
onclick={handleClick}>
Hide
</button>
)
### ref
You can use `ref` to get the hidden state.
Change `Content.tsx`
```typescript jsx
export function Content () {
const hidden = new Ref()
return (
<delay ref={hidden} hide={1000}>
{() => hidden.value.value ? 'hidden' : 'shown'}
</delay>
)
}
And change
app.tsx
```typescript jsx
import { State } from 'watch-state'const show = new State(true)
const handleClick = () => { show.value = false }
export default () => show.value && ( <>
<Content />
<button
onclick={handleClick}>
Hide
</button>
</>
)
## useParent
You can get parent HTML element inside a component
```typescript jsx
import { getParent } from '@innet/dom'
export function Content () {
console.log(useParent())
}
style
You can style components withstyle
function.
The function returns useStyle
hook.
Use this hook inside a component to get html-classes features on class
prop.```typescript jsx import { style, Style } from '@innet/dom'
import styles from './Content.scss' // or you can use an object like // { root: '...', header: '...', content: '...' }
const useContentStyles = style(styles)
export interface ContentProps extends Style {}
export function Content (props: ContentProps) { const styles = useContentStyles()
return (
<div class={() => styles.root}>
<header class={() => styles.header}>
header
</header>
<main class={() => styles.content}>
content
</main>
</div>
)
}
Then you can use `class` prop to define classes.
```typescript jsx
import { State } from 'watch-state'
const show = new State(true)
const handleClick = () => {
show.value = !show.value
}
export default (
<>
<Content
class={{
root: 'root',
header: ['header', 'another-class'],
content: [
'content',
() => show.value && 'show'
],
}}
/>
<button
onclick={handleClick}>
Hide
</button>
</>
)