Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@dy
Last active May 11, 2018 19:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dy/3b43f39d8ec26ebc1368742f37d27885 to your computer and use it in GitHub Desktop.
Save dy/3b43f39d8ec26ebc1368742f37d27885 to your computer and use it in GitHub Desktop.
Permanent component API research

jsxify

Convert JSX to JS

<header></header>
<main>
	<Compo>
		<div>Content</div>
	</Compo>
</main>
<footer></footer>
d('header')
d('main', {
	content: d('Compo', {
		content: d('div', 'Content')
	})
})
d('footer')

d is a DOM constructor, returning an html.

What is the solution for Compo?

Use-cases.

1. Slidy component - direct html update

let slidy1 = new Slidy()
let slidy2 = new Slidy()

d('main', {
	content: slidy1.render(props)
})

Q: How do we identify which one of the slidy components should be inserted - slidy1 or slidy2?

Using keys is the react/component-box solution:

function getCached(key, compo) {
	return cache[key] || (cache[key] = new Compo())
}
d('main', {
	content: getCached(props.key, Slidy).render(props)
})
  • component-box is existing pattern
  • a bit obvious code

Q: Is there a way to get rid of createSlidy(key)?

d('main', {content: Slidy(props)})

If there is a key in properties, the component is cached itself to a component box and on every redraw it is just fetched and rendered instead of creating a new instance.

  • that is really implicit and not obvious, natural expectation is new instance

If there is a key in props, compo is cached, otherwise (naturally) compo is returned by key. Ideally then we mark that as Slidy.create(props, key?) - that creates new component if the key is defined otherwise fetches instance by key.

  • we hide component-box functionality into every component and force users to implement that logic. Is that necessary? Is that is elegant?
    • extending CacheableComponent seems to be ok solution

Q: Is there an alternative solution to keys?

In principle that is responsibility of a component to know how it's content gets updated. Should that be a handpick update - it can update self content manually. Should that be a react.render - it can call it. Should that be a gl redraw - that can be it. Should that be a nanocomponent with component-box or some normal component cache - all that should be implemented withing the component itself.

But in essence that is the question of repeated JSX call, and we have to be able to identify JSX component → HTML element bound link. JSX creates HTML, once we call it second time - it looks at the existing HTML and tries to update it, whether it DOM diffing etc. Just returning new vdom is not sufficient.

Externally an app in essence just updates internal state and rerenders fully, even in react, it has to implement raw js strategy itself, it cannot return vdom.

✔ So JSX used within a component can be always implemented by component-box strategy, where user manually implements the strategy to optimize rerendering. JSX outside of component, like MDX or some static pass can directly be called without caching by key by creating an instance and render function.

2. vdom component, react component, any other normalized component

Implements vdom routine inside any way it needs. It does not have to export anything special but just render method updating content according to its state. container property is optional, but if the component is created without a container - it needs to be attached externally and its DOM should be available as component.element, eg.

new Compo({content: [
	'xxx',
	Compo2().render(),
	'yyy'
]})
  • if Compo2 is created without a container, it is detached and needs to be attached to Compo1 in a way.
  • that utilizes document.body container if container is undefined, which makes new empty component wrongly attached.

Q: what is good solution for nesting elements by parsing/converting JSX?

  • Possible solution - make render return an element instead of attaching to container.
    • that forces non-intuitive behaviour
    • in case if render does not return a thing, it breaks the content values.
  • Create fake temp element as a container/element for the component, like - that can be wrapped into component-box like construct.
new Compo({content: [
	'xxx',
	(() => {let e = dom('el'); Compo2({container: e}).render()})(),
	'yyy'
]})
  • foldable into component-box-like cache
  • can cache containers instead of components
  • Create stubs with ids and init components later on them, that allows for caching on per-element basis, like
new Compo({ content: [
	'xxx',
	dom('div#compo2')
	'yyy'
]})
new Compo2({ container: '#compo2', content: [
	dom('div#compo3')
]})

That all sounds too complicated, ideally we would have straghtforward solution - a function that just returns HTML, even if that HTML already exists, like

dom('compo', {
	content: ['xxx', dom('compo2', props), 'yyy']
})

This function would just create a tmp element, cache it, init component and render it into main DOM, in possibly async way. That would be too difficult for components to have that complicated API, but that is not for a tiny wrapper.

3. gl-canvas/regl-like component

Q: Considering most likely a wrapper function, what is the most possible use-case for gl-component?

render: () => <Scatter2d data={points} />
render: () => dom('Scatter2d', {data: points})

function dom (name, props) {
	if (name is component) {
		let compo cached[props.key] || createCompo({container: dom(el.name)})
		compo.render(props)
	}
}

That creates scatter container once, inits scatter inside and returns it every time.

Q: how do we deal with multiple gl-components with single canvas via jsx?

render: () => <Scatter2d {gl} /><Scatter2d {gl} />
  • we pass constructor options like container, gl etc. every time instead of once.

Q: Is there any JSX way to separate component constructor and rerender?

a. We can pass <Scatter2d constructor={opts} /> object, that is being ignored

  • not elegant, verbose, still same repeating

b. We can create elements separately beforehead and keep JSX calling component.render(props), whereas component is not the constructor but an instance.

<Scatter2d ...props/>  Scatter2d.render(props)

let scatter2d = new Scatter2d({ container: domContainer })
  • obvious natural way. q: How do we create not-existing elements then?
  • JSXify should be augmented with manual component constructors defining elements to insert, isn't it?
  • We can convert JSX into HTML string directly, ie. JSX → hyperx. Since hyperx does not support components, it is likely to convert them to stubs and init components later on, so that any vdom engine diffs them ok.
  • We can convert JSX components to vdom widgets.
  • We can use virtual-streamgraph https://www.npmjs.com/package/virtual-streamgraph example-like style
hx`<div>${ compo(props) }</div>`
  • For virtual-dom compatibility it is enough to implement virtual-widget interface https://github.com/Matt-Esch/virtual-dom/blob/master/docs/widget.md or https://github.com/yoshuawuyts/virtual-widget

    • that is too specific
    • that creates new widget every vdom update - we should instead do render
  • virtual-widget is very close and almost the same as nanocomponent - it creates an element and in update methods decides whether and element should be created once again or not

  • Strangely in virtual-widget update is called before the init, since init does not create a thing

Requirements for components API

Qualities

  • familiar, heartfelt, pleasant (родные)
  • concise, elegant, minimal (nothing to remove or no way to optimize more)
  • Provoking creativity
  • Not imposing difficult conventions
  • Unopinionated

Technical

  • JSX-compileable, renderable multile times
  • Foldable: responsible for the part of HTML
  • Compilable to VDOM: virtual-widget interface
  • Compilable to React: react-component interface

Correct inevitable logic of components, reincarnation of mods

! require('@mod/menu')

Components are responsible for part of HTML. HTML is exterior - the appearance, or the body. Component is the soul of the appearance - it runs the logic, the data, the activity. Forcing new appearance and taking care of what soul runs this or that region of HTML is soulless and error-prone: that inevitably loses data/life attached to part of the DOM.

Mods were initialized as a "soul" for HTML node, that runs the content and logic.

The method is making components responsible for part of HTML and asking them to update according to new state, and caching the components therefore according to the DOM elements.

  1. That allows for static HTML init: it looks up for components with data-component="Component" attributes and if these elements do not have component attached, inits them (constructor call).

  2. Diffing DOM to a new DOM structure saves as much of previous DOM as possible and augments/inits new components or destroys old ones.

  3. Updating app state just asks all the components to update their states.

So returning JSX

() => (
	<header></header>
	<Component>
		<div> text </div>
		<Component2></Component2>
	</Component>
	<footer></footer>
)
// ↓
() => (
	initComponents(
	h('header')
	h('div', {component: Component}, [
		h('div'),
		h('div', {component: Component2})
	])
	h('footer')
	)
)

Where h for components just creates html nodes with component attribute. Then components can be initialized/updated if they are not there, separately. So vdom is responsible for DOM, not for components, since that is not it's responsibility.

Q: how do we manage multiple gl-component instances on a single canvas in VDOM?

According to mods approach, single element may have multiple mods, ie. draggable and resizable, also scatter2d and line2d.

<canvas mods={[scatter2d, line2d]} ...params />

If both scatter2d and line2d get the full params object, that will be misleading, since both may have color param.

Another approach is

Which is significantly better since plot takes care of updating state. Also plot may be responsible for multiregl setup, that just handles a bunch of gl-components.

  • still that creates a problem for scatter2d and line2d since they create fake dom element for every instance.

Since we need scattergl/linegl components in a-vis not creating each one a new context, we need react portal-like functionality, eg.

<scatter2d {gl}/> <line2d {gl}/>

or

therefore element defines where element should be placed to.

With vdom-widgets an element can optionally return some html stub, just indicating that there is a renderer at this position, to handle construction/destruction/update.

Ideally we would not pass container for an element, if elements have adjacent placement.

That is possible to do only through external gl/avis-component initializer, that is not possible from within vdom-widget since we do not know siblings beforehead.

Q: Is gl-component something different from vdom-component or not?

Since we want them to be in a-vis, it is not.

Q: how should we organize jsx in a-vis?

a. Plot component creates a canvas container

  • we face a trouble of JSX nestedness here: child components take care about parents they should be inserted into, and hyperscript syntax does not allow portal to use that. That is the problem of gl-components not being a vdom nodes. Q: Does that mean we cannot nest them? That means they should be initialized independent of JSX, beforehead, and in JSX to be just called with render.

let plot = createPlot(canvas) let scatter = createScatter(plot.gl) let line = createLine(plot.gl)

<plot ...props> plot(props, [ <scatter ...props/><line ...props/> → scatter(props), line(props) ])

  • In this case we keep classic vdom-component compatibility as render(props, children?) - in case of the most components second argument is ignored.
  • That is the most natural setup, basically we just transform JSX to natural creation
  • User manually takes care of keying/caching instances if required. → It needs to be documented how to create JSX setup for a-vis → It needs to prove and test react/preact setup → Put it to permanent readme

We cannot join whole components init part and render part, since children components may depend on the parent setup, they are not self-sufficient.

b. Direct component creates its own container → new Scatter(canvas)

c. Portal components may share container

Q: can we use vdom stubs for the matter? Ideally components would care about real html only on init and not care about dynamic layout recreation.

let scattersvg = ScatterSVG({ container: html('svg') })

→ scattersvg.render() // all svg vdom update happens inside

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment