@innet/dom
v0.15.0-alpha.4
Published
Tools to build Web Site
Downloads
97
Readme
@innet/dom
Abstract
This is an innet
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 start innet-dom
app development.
npx innetjs init my-app -t fe
change my-app to work folder name
Go into my-app
and check README.md
Handler
Use dom
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.
export default (
<h1>
Hello World!
</h1>
)
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.
<!doctype html>
<html lang="en">
<head ... >
<body>
<div id="app"></div>
<!-- add this ^ -->
</body>
</html>
And change app.tsx
const app = document.getElementById('app')
export default (
<portal parent={app}>
<h1>
Hello World!
</h1>
</portal>
)
You can use portal
everywhere inside the app.
Change app.tsx
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
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
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
export const Content = () => (
<h1>
Hello World!
</h1>
)
Change app.tsx
import { Content } from './Content'
export default (
<Content />
)
props
Any component gets an argument props
.
If props have not provided the argument equals undefined
else you get an object that contains the props.
Change Content.tsx
export function Content ({ color }) {
return (
<h1 style={{ color }}>
Hello World!
</h1>
)
}
Then you should use the color
prop outside.
Change app.tsx
import { Content } from './Content'
export default (
<Content color='red' />
)
Hooks
You can use hooks inside a component. Sync hooks should be used before await
,
async hooks should be used as the first await
.
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.
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 take useChildren
.
Change Content.tsx
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
import { Content } from './Content'
export default (
<Content color='red'>
Hello World!
</Content>
)
Return
A component awaits a return:
string
,number
- render as text nodeconst Test1 = () => 123 const Test2 = () => '123'
null
,undefined
,boolean
,symbol
- ignoreconst Test1 = () => null const Test2 = () => {} const Test3 = () => true const Test4 = () => Symbol()
- DOM Element - put in the DOM
const Test = () => document.createElement('div')
- JSX Fragment,
array
- render contentconst Test1 = () => <>content</> const Test2 = () => ['content']
- JSX Element - put in the DOM
const Test1 = () => <div>content</div> const Test2 = () => <br />
- JSX Plugin - run plugin
const Test1 = () => <portal parent={app}>content</portal> const Test2 = () => <slot>content</slot>
- function - observable children
const state = new State() const Test1 = () => () => state.value const Test2 = () => state const Test3 = () => <>{() => state.value}</>
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.
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.
async function Content () {
const { text } = await fetch('...').then(e => e.json())
return <div>{text}</div>
}
innetjs helps to make code splitting.
async function Content () {
const { Test } = await import('./Test')
return (
<div>
<Test />
</div>
)
}
Test.tsx
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 use yield
instead of return
async function * Content () {
yield 'Loading...'
const { text } = await fetch('...').then(e => e.json())
yield <div>{text}</div>
}
Generic Component
It can be useful when you want to do something after a content deployed.
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.
function * A () {
queueMicrotask(() => {
console.log(
'queueMicrotask A',
document.getElementById('a'),
document.getElementById('b'),
)
})
yield <span id='a' />
console.log(
'generic A',
document.getElementById('a'),
document.getElementById('b'),
)
}
function * B () {
queueMicrotask(() => {
console.log(
'queueMicrotask B',
document.getElementById('a'),
document.getElementById('b'),
)
})
yield <span id='b' />
console.log(
'generic B',
document.getElementById('a'),
document.getElementById('b'),
)
}
function Content () {
return (
<>
<A />
<B />
</>
)
}
You get the next output:
generic A <span id="a"></span> null
generic B <span id="a"></span> <span id="b"></span>
queueMicrotask A <span id="a"></span> <span id="b"></span>
queueMicrotask B <span id="a"></span> <span id="b"></span>
Ref
Ref
helps to get an HTML element.
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 by onDestroy
from watch-state
Change Content.tsx
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 second
onDestroy(() => clearInterval(timer))
// stop timer on destroy
return () => count.value
// return observable value
}
And change app.tsx
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
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
import { Content, color } from './Content'
export default (
<>
<Content>
Without context
</Content>
<context for={color} set='red'>
<Content>
With context
</Content>
</context>
</>
)
show
You can use show
element to show/hide content by state.
import { State } from 'watch-state'
const show = new State(true)
export default (
<show when={show}>
<button
onclick={() => {
show.value = false
}}>
Click Me
</button>
</show>
)
when
can be:State
|Cache
|() => any
|any
hide
You can use hide
element to show/hide content by state.
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 use switch
element to show a content by string state.
import { State } from 'watch-state'
const str = new State('')
const case1 = () => {
str.value = 'case1'
}
const case2 = () => {
str.value = 'case2'
}
export default (
<switch of={str}>
<slot name='case1'>
Case 1
<button
onclick={case2}>
Next
</button>
</slot>
<slot name='case2'>
Case 2
</slot>
Default content
<button
onclick={case1}>
Next
</button>
</switch>
)
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.
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.
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 (
<ul>
<map of={names}>
<User />
</map>
</ul>
)
Use key
property to improve DOM
changes when you use an array of objects with some uniq field, like id.
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.
import { useChildren } from '@innet/jsx'
export const Content = () => (
<slots from={useChildren()}>
<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>
</slots>
)
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.
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.
export default (
<Content>
<slot name='header'>
Custom header
</slot>
Custom content
</Content>
)
You can use a couple of slots with the same name.
export default (
<Content>
<slot name='header'>
Custom header1 <br />
</slot>
<slot name='header'>
Custom header2
</slot>
Custom content
</Content>
)
router
You can render content by url.
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 Found
If you want to show Settings page
on /settings/test
, use ish
prop on router element
export const Content = () => (
<router ish>
<slot name='/'>
Home page
</slot>
<slot name='settings'>
Settings page
</slot>
Not Found
</router>
)
When you use a router, that is inside a slot of another router, the route checks the next peace of url path.
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 Found
You can use search
prop to make router binds on query search params
export const Content = () => (
<router search='modal'>
<slot name='login'>
Login
</slot>
<slot name='logout'>
Logout
</slot>
</router>
)
?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
.
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-other
a
The tag a
has a couple of features.
rel="noopener noreferrer nofollow"
andtarget="_blank"
are default for external links.
href
If href
starts from /
, ?
or #
then the Link will use History API.
export const Content = () => (
<div>
<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>
</div>
)
replace
By default, it pushes to history, but you may use replace
to replace current history state.
export const Content = () => (
<a replace href="/">
home
</a>
)
class
You can add root or active link class
const classes = {
root: 'link',
active: 'active',
}
export const Content = () => (
<div>
<a
href="/"
class='only-root'>
home
</a>
<a
href="/test"
class={classes}>
test
</a>
</div>
)
You can use all features from html-classes for the class
prop.
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 with href
prop value, but use exact
to compare exactly.
const classes = { root: 'link', active: 'active' }
export const Content = () => (
<div>
<a
href="/"
exact
classes={classes}>
home
</a>
<a
href="/test"
classes={classes}>
test
</a>
</div>
)
scroll
You can use smooth scroll
body, html {
scroll-behavior: smooth;
}
The property of scroll
says should we scroll on click and how.
by default equals
before
export const Content = () => (
<div>
<a href="/" scroll='before'>
home
</a>
<a href="/test" scroll='after'>
test
</a>
<a href="?modal" scroll='none'>
test
</a>
</div>
)
scrollTo
If you want to scroll the page to custom position (by default it's up of the page) use scrollTo
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.
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
import { useHidden } from '@innet/dom'
export function Content () {
const hidden = useHidden()
return () => hidden.value ? 'hidden' : 'shown'
}
And change app.tsx
import { State } from 'watch-state'
const show = new State(true)
const handleClick = () => {
show.value = false
}
export default () => show.value && (
<delay hide={1000}>
<Content />
<button
onclick={handleClick}>
Hide
</button>
</delay>
)
ref
You can use ref
to get the hidden state.
Change Content.tsx
export function Content () {
const hidden = new Ref()
return (
<delay ref={hidden} hide={1000}>
{() => hidden.value.value ? 'hidden' : 'shown'}
</delay>
)
}
And change app.tsx
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
import { getParent } from '@innet/dom'
export function Content () {
console.log(useParent())
}
style
You can style components with style
function.
The function returns useStyle
hook.
Use this hook inside a component to get html-classes features on class
prop.
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.
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>
</>
)
Issues
If you find a bug or have a suggestion, please file an issue on GitHub.