Yet another JavaScript framework that nobody needs. Instead of re-rendering whole components after state value changes,
it updates only areas in the DOM defined by the developer as affected by state changes.
The plan is to create an SSR-mode that shifts HTML rendering to the server completely. This means that only JavaScript,
that does not render HTML will be bundled and send to / executed on the client.
Warning
Cherry-soda is experimental. Everything is subject to change.
If you just to test out cherry-soda, you can run the examples. For that you need to have Node
installed. Then, clone the repository, go to /example
, and install the dependencies with npm i
there. Start an
example like this:
npm run start ./cherry-soda-template
In a new Node project install cherry-soda with npm i cherry-soda
, and add files src/index.js
and src/App.js
:
// src/index.js
import {mount} from 'cherry-soda'
import App from './App'
mount(<App />, document.querySelector('#app'))
if you use webpack HMR, append
if (module.hot) {
module.hot.accept();
}
// src/App.js
import {defineDom} from 'cherry-soda'
export default function App() {
defineDom(<h1>Hello world!</h1>)
}
index.js
is the main entry point. The mount()
function renders the given JSX element to the given DOM element. Each
component can render a JSX element to the DOM itself by calling defineDom()
.
Use webpack (or a bundler of your choice) with a configuration like this:
import webpack from 'webpack'
import HtmlWebpackPlugin from 'html-webpack-plugin'
module.exports = {
entry: 'path/to/src/index.js',
output: {
filename: 'bundle.js',
path: 'path/to/dist/',
publicPath: '/'
},
module: {
rules: [{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/env'],
plugins: [['@babel/plugin-transform-react-jsx', {
runtime: 'automatic',
importSource: 'cherry-soda'
}]]
}
}
}]
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(mode)
}),
new HtmlWebpackPlugin({
templateContent: '<div id="app"></div>'
})
]
}
Of course, add SCSS, Less, TypeScript, etc. according to taste.
Notice the HtmlWebpackPlugin: You'll need an HTML file that
imports the bundled script and has a mounting point (in this case a <div>
) with id="app"
. Make sure bundle is
executed after the document is loaded, or adjust your src/index.js
so that mount()
is called after document load.
Cherry-soda exports createElement()
which allows you to create virtual elements, that form a
template for rendering the actual DOM later on. createElement()
is exported as jsx
in jsx-runtime.ts
, so you can
call it by using JSX:
// this
import {createElement} from 'cherry-soda'
const myDivElement = createElement('div')
// is the same as
const myDivElement = <div />
Where
VNodeType = | (props: Record<string, any>) => void | typeof Fragment | string
Creates a cherry-soda element.
Parameters:
type: VNodeType
– Either the tag name of a DOM element, theFragment
symbol, or a function (that may or may not calldefineDom()
).Record<string, any>
– Either attributes for a DOM element, or properties passed to the function passed astype
.
Returns:
VNode
– AVNode
with the giventype
&props
.
This function should only be called in a function component, and should at most be called once per function component, otherwise might break things.
Defines the JSX element node
for the current function component. Converts internally to DOM elements and renders to DOM appropriately.
Parameters:
node: JSX.Element
– JSX element to be defined for the function component.
Returns:
JSX.Element
– The node that was passed asnode
.
Defines the JSX element node
for the given DOM element to
. Converts internally to DOM elements and renders to the DOM element appropriately.
Parameters:
node: JSX.Element
– JSX element to be defined for the given DOM element.to: HTMLElement
– DOM element to which elements should be rendered.
You can create states with state()
. This will return a State
object that holds a value. During
rendering states are automatically converted into their respective values. They can also drive dynamic values
Where
StateMethods<V> = { [method: string]: (currentValue: V, ...rest: any[]) => V }
and
UpdaterInitializer = (update: (newValue: V) => void) => void
Creates a State
object with the given value.
The methods
parameter allows you to define methods to update the state value besides calling .update()
. For example,
you might want to create a toggle:
// normal state update
const isPlaying = state(false)
button.on.click(() => {
isPlaying.update(!isPlaying.valueOf())
})
// with method
const isPlaying = state(false, {
toggle: (currentValue) => !currentValue
})
button.on.click(isPlaying.toggle)
To have a state being updated, you can set an updater with the updater
parameter. The updater has to be a function,
which gets an update()
function passed to it. For example, you might want to tie a state value to an element's
dimensions:
const elementDimensions = state(myElement.getBoundingClientRect(), {}, update => {
const resizeObserver = new ResizeObserver((entries) => {
update(entries[0].contentRect)
})
resizeObserver.observe(myElement)
})
Parameters:
initialValue: any
– The initial value for this state.methods?: StateMethods
– Functions that can be called on this state to update it.updater?: Function
– Function for setting up updaters.
Returns:
State
– AState
object withinitialValue
as its value, andmethods
as methods.
The State
object holds the initial value of the state and can be used in the DOM like a value:
import {defineDom, state} from 'cherry-soda'
function Component() {
const myState = state('foo')
defineDom(
<div id={myState}>
{myState}
</div>
)
}
Sometimes, you might not want to use the states value directly, but a transformed version of it. To do that, call
the .use()
method. It takes a callback to which the state value is passed as a parameter:
import {defineDom, state} from 'cherry-soda'
function Component() {
const elementType = state('a')
const label = 'foo'
defineDom(<>
{elementType.use(tagName => {
if (tagName === 'a')
return <a>{label}</a>
else
return <span>{label}</span>
})}
</>)
}
You can also process multiple states in one .use()
by concatenating them with .and()
. The state's values are passed
in the order they were concatenated in:
import {defineDom, state} from 'cherry-soda'
function Component() {
const revenue = state(5)
const expenses = state(3)
defineDom(<>
Profit: {revenue.and(expenses).use((rev, exp) => rev - exp)}
</>)
}
Tip: using
myState
in JSX is the same asmyState.use()
is the same asmyState.use(value => value)