dy / spect

Observable selectors in DOM
https://dy.github.io/spect
MIT License
77 stars 8 forks source link

Aspect indicators in HTML/JSX #23

Closed dy closed 5 years ago

dy commented 5 years ago

The problem of aspects is that they aren't good as primary elements, they perfectly augment some ready object. That makes them a perfect addition to h function, but not the h itself.

What are the possible approaches to organize aspect-enabled components?

1. Class/Id tokens

h`
<div.text-input.autosize ...>
<div.date-range-input ...>
<div#grid ...>
`

2. Direct component

h`
<${[TextInput, Autosize]} ...>
<${DateRangeInput} ...>
<${Grid} ...>
`

3. is, use attributes

h`
<div use=${[TextInput, AutoSize]} ...>
<div use="date-range-input" ...>
<div is=${Grid} ...>
`

4. Custom elements, least-nonstandard

<text-input class="autosize" ...>
<date-range-input ...>
<data-grid ...>

? What's the way to register multiple aspects? Possibly least-nonstandard classes?

5. @ attribute

h`
<div @text-input @autosize ...>
<div @${DateRange} ...>
<@${DataGrid} ...>
`

6. @-prefixed classes

h`
<div class="@text-input @autosize ..." ...>
<div class="@DateRange" ...>
<${Grid} ...> 
`

7. Generated classes

h`
<div class="${TextInput} ${Autosize}" ...>
<div class=${DateRange} ...>
<div id=${Grid} ...>
`

8. Disallow $ function, make h main entry as

import $ from 'spect'

// date-range aspect
$`
<@DateRange>${el => {
}
}<//>
`

// main aspect
$`
<div class="@TextInput @Autosize" ...>
<div class="@DateRange" ...>
<div id="@Grid" ...>
`

9. Attributes

$`
<div text-input=${params} autosize=${params} ...>
<div date-range=${params} ...>
<div grid=${} ...>
`
dy commented 5 years ago

N. Tachyons/css- inspired framework approach

Tachyons is a set of standard mini-classes. You don't invent a classname to apply tachyon, you just apply it. Same way spect could have a set of mods - known components/behavior particles, like draggable, tooltip etc. Spectacle? <div class="@draggable @tooltip"...>. But in this case we need padding params. Instead, would be easier to have attribute <div class="...aspects ...tachyons" draggable=${params} />, which is unlike tachyons - they define aspect+param+value at the same time: <div class="drag-speed-2 mt-3"></div>, so - denormalized cached often-used particles.

Such particles are registered somewhere and take signature. .mt-${param}: { behavior }. What construct best describes such particles?

Object

{
[`pfx-${param}`]: (el, param) => {}
}

Tpl literal

x`
pfx-${param}: ${ (el, param) => {} }
`

Function

function pfx (param) {
  return (el) => {}
}

JSX literal

h`
<.pfx-${param}>
${}
<//>
`

Router

Vue, express, react-router: https://github.com/pillarjs/path-to-regexp

{
'.m:side-:size': (el, side, size) => {}
}

Printf placeholders

'.m%w-%d': (el, side, size) => {}

String-template placeholders

'.m{w}-{size}': (el, side, size) => {]

Generally speaking

An identifier, set of params and handler.

dy commented 5 years ago

How to organize passing params

How to organize identification

  1. As [prefixed] class <div class="element"> <div class="@element">
    • a case of 2.
  2. šŸŒŸ As [prefixed] attribute <div element> <div aspect-element> <div data-element>
  3. As standard attribute <div is="element"> <div use="element">
    • difficult to fit all aspects into narrow custom elements scope.
    • does not allow standard multiple aspects
    • Identifying is or use is possibly slower than classname, although there's hidden attribute example.
    • is and use is a partial-case of 2.
  4. As direct tagName <element>
    • tagNames should be standard HTML elements to have ground for reader - you always know that the code is at least safe html. Having custom elements or react components looses feeling of safety - you no more know for sure how this or that component works, because that is not documented and against standard practices. So not 4.

Is that profitable to separate identification from passing params?

How to organize project usage, ie. js structures?

dy commented 5 years ago

How to separate creation of aspects from using of them?

$`<some-el>` - usage.
$('some-el') - definition.

Lol. Plain strings are for creation. Template literals are for usage. But - how do we employ htm to do that?

let html = h.bind($) - turns everything into definition mode, instead of usage.

Then we can use dream solution - create method.

// that doesn't start observing.
let aspect = $.create('#app, .app', fn)

// this triggers observing
$('#app', aspectFunction)

But then that's redundancy. We could run as

$(aspect)
// or
$`<${aspect}/>`

which would be a component-compatible invocation. But that would break classical notation $(selector, function).

We could make let aspect = $(selector, fn) an anonymous aspect, instantly runnable, and let aspect = $.create(selector, fn); $(aspect) for registered aspects. But that's redundancy of signatures, no clear difference $.create and $.

All that is just to separate $('div') as registering selector-aspect from $('div') as creating html structure, or $('custom-el') as an element-aspect from $('custom-el') as creation.

If we remove registering from $ completely, prohibiting constructs like $('#app', ...rest), we complicate binding h to real DOM:

let app = $.create('#app', el => {
})

$`<div id="#app"></div>`

In fact though that makes html really clean, simple and valid, without weird connections like <#app><//>. Although that conceptually breaks portals $`<${document.body}><//>`, also makes insertions impossible: $`<${aspect}>...<//>`.

We could remove html logic from spect completely, to htm and soft-h functions.

We could identify first-level tokens as binding, the rest as hyperscript:

$`
<${binding}>
  ${ html creation code, inc. aspects }
<//>
`

But then first-level code would never be able to create hyperscript, and not-first-level code would never be able to register listeners:

// this attaches nested div element to all div elements (which is confusing - <el> is always expected to be a tangible real element, not fake binding!)
$`
<div>
  <div></div>
</div>
`

// this makes no sense (indeed selectors within selectors - what's the use-case? Although - portals do that, and any side effects)
$`
<${document.element}>
  <#app>${123}<//>
<//>
`

What if we'd separate tpl from binding as

$('div')`htm`
$(el)`htm`

// that's the same as 
$('div', htm)

That still seems to be two separate functions - creation h and observing $.

dy commented 5 years ago

What if we make mounting an aspect too? <${target}><//> is actually a type of "portal" aspect (host, root, mount), and should be handled by the same rules:

h`
<div mount=${document.body}>...</div>
`
// then the aspect is
$('[portal]', el => {
  let target = attr('portal')
  target.appendChild(el)
})
dy commented 5 years ago

Solution

dy commented 5 years ago

How do we connect function-aspect to html?

`<div is=${header}><//>` // h('div', {is: header}) is selectable by aspect, but difficult to implement $('[is]', el => { ??? }) - the attr is serialized function.

`<div>${header}<//>` // h('div', header) ? isn't that weird that we define aspect as a child of node? Is child an aspect? Maybe, but not every aspect is a child. If we'd called lib mod, children wouldn't be mods.

`<${header}><//>` // h(header) What element should be passed to aspect? Some undefined one? Like custom elements.

`<div id=${header}></div>` Confusing! Why function stands as id? Also that's custom handling of ids.

That is the problem caused by allowing aspect to be any selector instead of strictly defined one.

For now the only way to connect aspects - is via loose identifying.

`<div#id></div>`

Splitting to $ and h introduces that loose contract and prohibits use of functional insertions, instantly registered as aspects, because that makes h quirky aspect-cases enabled.

Actually, custom elements is quite legal way to attach aspects:

$('#header', function header (el) {...})
h`<header id="header"></header>`

//reduce boilerplate iteration 1
$('header', header)
h`<header/>`

//reduce boilerplate iteration 2
h`<${header}/>`

wrong. That undermines the principle of grounded html, when the user can be grounded with generated code. Possibly we should prohibit components insertion.

Since aspects observe real DOM, they can't know if custom attribute, like function, was inserted. ? h could possibly take handle of notifying aspects about creation.

function highlight (el) {}

// registers highlight as random string, sets observing that string
h`<a ${highlight}></a>`

But how'd we pass custom non-stringy props to that aspect? No way, right? Because aspects are not elements - therefore they can't take props passed to them, they can only read whatever's assigned to element, or accept own propset in some way. Angular registers inputs for that purpose, we use hooks. But htm renders attributes, so we read attrs via hooks, whereas react and angular read props. So our htm would render props, which would not be a direct hyperscript. So basically we insert hooks into our hyperscript function to enable aspects take non-stringy props.

(! mb abandon custom h implementation for standard one and focus on aspects. Eg. react could easily be integrated via refs as <a ref=${aspect}></a>)

. that's a bit confusing as well to have aspects registered as direct elements, not at the point of registering $('header') - it's all clear here, but at the point of declaring <header></header> - that's completely unobvious that the standard element will be augmented with aspect. Even with css that's confusing practice and should be left to narrow normalize/polyfill set.

. since on has signature of selecting target, mb that's meaningful to describe aspects aside from the main html code, and in main html code just put markers.

$(document.body, () => {
$('#app', el => { ...init })
$('[draggable]', el => { ... init draggable })
$('[storage]', el => { ... connect storage })
$('[tooltip]', el => {... connect tooltip })

html`
<div id="app" class="mr1 ml2" draggable tooltip="abc" storage="abc">
</div>
`
})

Stuffing that code into html would make it a mess. But on the other hand that creates bindings boilerplate like

$('#app', () => {
  $('[sidebar]', aside)
  $('[header]', header)
  html`<aside sidebar/><header header/>`
})
function aside () {}
function header () {}

// that could've been minimally done as
// that is more expressive and way easier to get started, similar hscript on steroids
$`<#app><${aside}/><${header}/><//>`

// which is
$('#app', html`<${aside}/><${header}/>`)

// which is with html complacency ground is
$('#app', html`<aside app-sidebar/><header app-topbar/>`)

// which is redundant description, since sidebar/topbar functions already may have names
// react-way aspects would be
<aside Aspect/><header Topbar/>

// htm-way aspects are unavoidably
h`<aside ${sidebar}/><header ${Topbar}/>`
// or if aspects are registered
h`<aside sidebar/><header topbar/>`

?! <div@header@mutable/>? - messy strings ?! html-aspect modifier html`<${document.body}><div#app></div><//>`? - same as mount aspect.

dy commented 5 years ago

So very expressive way of aspects is

$`
<#app>
  <header is=${topbar}><//>
  <aside is=${sidebar}><//>
  <main path=${
    {
      '/': home,
      '/signIn': '/sign-in',
      '/singin': '/sign-in',
      '/sign-in': () => import('./sign-in.js'),
      '/dashboard': () => import('./dashboard.js'),
      '/catalog': () => import('./catalog.js'),
      '/users/:userId': (el, {userId}) =>
    }
  }/>
</#app>
`

So, aspects provide ground for mods, which are a set of special attributes, allowing really graceful h-script almost without js. Such mods as: target, is, route etc - all of them provide token and take config.

dy commented 5 years ago

! observation: htm is so cool - it takes attributes for no-tag fragment-ish elements, which can possibly take arguments:

`<mount=${target}></>`

// which is the same as fragment with key
< mount=${target}></>

// which makes aspect === initial element, or direct/main aspect (suuuper cool)
<div mount=${target}></div>
<mount=${target}></div>
// ā†‘ these are not exactly different, because fragment is mounted to target, same as div element
// also that makes registering direct aspect meaningful:
$('div') => `<div>` - in sense of create that aspect.

That enables natural separation of creation/deletion: h(tagname?, {container?, ...aspects}, children) - natural flow: what, how, what's next. And registering aspects is just an external tool.

We don't need selectors to register aspects. We can just directly do $.create('aspect', handler), and it will be processed one possible way only, to select as <div aspect/>, not id/class/etc.

dy commented 5 years ago

Aspectful HTML logic explained.

<div></> - fragment with single main `div` aspect
<div=${options}/> - fragment with single main aspect with options
< div=${options}></> - fragment with single secondary aspect with options
<div>${children}</> - fragment with `div` and `children` aspects, same as <div children=${children}/>
<children=${children}/> - fragment with single main children aspect, same as <>${children}</>

Insertion:

< ${aspect}></> - dynamically inserted secondary aspect
<${aspect}></> - dynamically inserted primary aspect
<>${aspect}</> - fragment with children aspect options, same as < children=${aspet}/>
<${aspect}=${options}/> - dynamically inserted aspect with dynamic options

Since tree logic is an aspect too, with reserved children name, there are consequences:

<parent=${aspect}></>, <div parent=${}></> - for ensuring parent container.
<parent=.selector>${el=> {}}</> - is the same as <parent=.selector children=${el=> {}}/>

Insertion function logic:

<${el => {}}></> - el is fragment here
< ${el => {}}></> - impossible with htm, needs forking, el is fragment
<x=${() => {}}></> - fn is just aspect options, no result
<>${el => {}}</> - < children=${el => {}}/>

React has children prop always passed to props object. Therefore the pattern is passing all aspects as props, not the main aspect.

<${ ({element, children}) => {} }/>
<>{$({element, children}) => {}}</>

But - children is already a property of element, that's duplication; also, the rest aspects are almost always present as props on element directly. Also, there's no standard element aspect, so that makes a contradiction. Finally, it's just inconvenient syntax. So, the insertion pattern is the following:

<${el => {}}></>
<>${el => {}}</> === < childNodes=${...}></>

So, every standard aspect is just a prop of element {aspect1, aspect2: options2, ...aspects} === <aspect1 aspect2=${...} ...aspects/>. This way, mounting is <parentElement=${}></> or a custom aspect.

! functional aspects: <appendChild(${args})>

! xslt inspiration: https://en.wikipedia.org/wiki/XSLT_elements

Eg. xslt defines an aspect namespace: </>. If we can pass existing object/element as main aspect: <${document.body}></>, we can use that as any other aspect: <div ${document.body}></>.

Actually, we can think of having main aspect reserved for DOM-related reality. Eg. if we have non-DOM, but some other reality, the main aspect there would be something different. Like sound, or syntax trees.

<{main-reality-aspect} {other-realities-aspects}>{sub-aspects}</>

So, such direct aspects are anonymous <${fn1} ${fn2}></> - they apply some action on created container. If we pass something other than a function - we consider that object an aspect, like class or other. Passing the other aspect in place of main aspect <${document.body}/> acts like an alias, or <augment=${document.body}></augment> - so we describe additional aspects to body.

Precedents of using refs in html:

To extend every element in DOM, matching some selector, we should provide some special aspect: <šŸ‘=".class"></>. But since we reduce selectors to attributes only, we can pick simple slot-like property:

<aspect="app">Ensure content ${el => `and functionality`}</aspect>
<div app/>

// which is the same as
$('[app]', el => h`Ensure content`)

So which form is better:

<aspect=app>...</>
<aspect name="app"></>
<aspect selector="[app]"></> +custom-element-able
<$="[app]"> +selector +spect +jquery +theoretically pure -bad attr name -incompat with html

<div aspect="app other"></div>
<div app other></div>

Notes:

- <children="#other-el"></> - inserted children are taken as other element by selector/URL, as SVG href. Not the same as <>#other-el</>, so children can't have reference, that is direct value. Actually, `children` is not assignable prop.
- <use href="URL"></use> - meaningful primary aspect, could be interesting to mix it to other aspects.
- <div span/> -  primary aspects aren't always mixable. Although would make sense sometimes <nav ul><li a></></>
- <children></> - refers to nested nodes (not in HTML though, in DOM), therefore <><parentNode></> - refers to outer node (not in HTML though, in DOM). These props are weird aspects:
- <parentNode=${el => el.parentNode}/> - that rewrites parent, so that isn't exactly the real DOM children, that's an overriding alias aspect, same as <x children=${el => el.childNodes}>abc</> -
that can override `abc` children, so - duplicate declaration.
So possibly we could mark such technical aspects somehow (actually they're both read-only).
- <! is used for aspects from another reality - <!doctype, <![CDATA[, <!-- comment.
- <main><path/></> is the same as <main path/> - aspects propagate up, being defied on shallow document fragments.
- <x ${fn}> - anonymous aspects cannot take properties.
- <${el} ${el}></> - makes one element a prolongation of another. There are some rules to aspect combinations.

If we pick <aspect> custom element, it forwards us to #25.

dy commented 5 years ago

What if we make :parent a pseudo, similar to :is/:where.

dy commented 5 years ago

Seems that having aspects registered via custom-element is not very profitable strategy - not much we can do by just describing it in pure html - just registering them is not expressive and doesn't give much profit. Instead, we can focus on registering aspects and hyperspect engine, providing h function for aspect-oriented HTML.

It's sad though that the only thing that separates $ from h is this inconsistency of first selector.

$('#app', el => h`
  <${el}>
    <header ${topbar} title="App title"></>
    <aside ${sidebar}></>
    <main ${el => el.path = history.location}/>
  </>
`)

//same as
$`<!app>
    <header ${topbar} title="App title"></>
    <aside ${sidebar}></>
    <main ${el => el.path = history.location}/>
  </>`

Similar to passing another aspect/target to provide aspect $`<${el}></>` (that is a special type of anonymous aspect), we could whether pass some special object $`<${sel()}></>` or just special type of string (selector) <#app></>, or have some token <@app></>, or explicit form <a-spect selector=${}> or etc. That doesn't save from the situation though when we need to deal with html:

$`<#app>
${el => h`<${el}>...</>`}
</>`

So, h function must be an effect, similar to on. With on we can:

on('click', () => {}) // registers for current element
on(el, 'click', () => {}) // same
on('.class', 'click', () => {}) // more generic registerer

html('.class', `...`) // make html content for an element.
dy commented 5 years ago

Finalizing. There's no $ function, there's a set of effects, html is one of them. The effects by default take context of outer effect, unless selector is explicitly defined.

html`content` - put directly to body
html('#el', `content`) - put into element
html('#el', `<div>${ html`content` }</>` - put into `div` element

That is similar to plain html. If you define some html code, it's placed directly into the container element.

dy commented 5 years ago

Maybe it's easier to split up $ and html/fx for clarity. $ defines target context, effects apply contextual actions. on(selector?, event, handler) is not very efficient with that signature - doesn't look like delegate.

$(selector, el => {
  on('click', 'em', handler) // delegates to local em
  // or on('click', handler, [delegate])?
  on('click', handler) // attaches to element directly
  html`Ensure <em>contents</em> <...> - works as html hook/reducer`

  // scopes to another target, attaches initial handler
  $(document.body, el => on('click', handler))
  $('#side-area', el => html`<...>Additional side-effect html`)
})

That +keeps effects clean +keeps context setter clean +keeps spect signature +makes spect-less effects effectless +adds main exports

Yep. A way to go. Direct HTML can be achieved as $(document.body, el => html``).