Coder Social home page Coder Social logo

spect's Introduction

subscript spect    npm bundle size npm

Observe selectors in DOM.

spect( container=document, selector, handler? )

Observes selector in container, invokes handler any time matching elements appear.
Handler can return a teardown function, called for unmatched elements.
Returns live collection of elements.

import spect from 'spect';

// assign aspect
const foos = spect('.foo', el => {
  console.log('connected');
  return () => console.log('disconnected');
});

// modify DOM
const foo = document.createElement('div');
foo.className = 'foo';
document.body.append(foo);
// ... "connected"

foo.remove();
// ... "disconnected"

spect(element[s], handler)

Listens for connected/disconnected events for the list of elements. (alternative to fast-on-load)

const nodes = [...document.querySelectorAll('.foo'), document.createElement('div')];

// assign listener
spect(nodes, el => {
  console.log("connected");
  return () => console.log("disconnected");
});

document.body.appendChild(nodes.at(-1))
// ... "connected"

nodes.at(-1).remove()
// ... "disconnected"

Live Collection

Spect creates live collection of elements matching the selector. Collection extends Array and implements Set / HTMLColection interfaces.

const foos = spect(`.foo`);

// live collection
foos[idx], foos.at(idx)                       // Array
foos.has(el), foos.add(el), foos.delete(el)   // Set
foos.item(idx), foos.namedItem(elementId)     // HTMLCollection
foos.dispose()                                // destroy selector observer / unsubscribe

Technique

It combines selector parts indexing from selector-observer for simple queries and animation events from insertionQuery for complex selectors.

Simple selector is id/name/class/tag followed by classes or attrs.

  • #a, .x.y, [name="e"].x, *, a-b-c:x - simple selectors.
  • a b, #b .c - complex selectors.

Alternatives

element-behaviors, insertionQuery, selector-observer, qso, qsa-observer, element-observer, livequery, selector-listener, mutation-summary, fast-on-load, selector-set, rkusa/selector-observer. css-chain

spect's People

Contributors

dependabot[bot] avatar dy avatar hamirmahal avatar jafin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

spect's Issues

spect/react

React functional subset built with spect.
Just functional components, hooks and vdom.

function render(vdom, target) {
  target.append(...vdom)
}

Can take on react/preact tests.

t-mod

Simple translator bound to current local

(t) => {}

DOM engine challenge

  • Implement html effect with different engines

snabbdom

  • tricky to implement html reducers / html node refs without tovdom call every time

nanomorph

  • works with reducers
  • need to reconstruct full dom every rerender

incremental-dom

  • static props are perfect for div#id.class
  • currentElement tracking allows for attaching aspects

Necessity of $ / manual context control

$ raises some problems:

  • interference with jQuery, making impossible to use aspects along with it
  • naming inconsistency: create, update, destroy, but now $, update, destroy
  • redundant selector function - can be just create(document.querySelectorAll(selector), fn)

In all that, there's not enough value in it.

Alternatives:

create($(sel), fn) / spect(els, fn)

Mb as low-level machinery is ok, but no value over $.

  • $ as picking up a collection is separation of concerns, attaching aspect to element is another concern

html<#target>content</>

  • initial idea
  • allows spreading attrs on targets
  • direct target drawbacks (which ones?)
  • no JSX support

live observer, mount effect is component entry

  • get rid of mount effect
  • no off-dom aspects

let foo= spect(fn); foo(el)

  • wrapping is fine, but more calls, compared to direct spect(el, ...aspects)

foo(el)

  • Impossible to figure out arguments of caller from inside the effect, only by static compiling

Internal directives 🌟

function foo (el) {
  spect(el)
  html` ...el content `

  spect(portal)
  html`...portal content`
}
  • allows easy switch of current context
  • no need for .call thing
  • turns spect into a valid effect (enables current context)
  • allows multiple rendering targets within single call
  • similar to canvas2d rendering context: beginPath, endPath
    ? mb call it at? ctx? curr? context?
function foo (el) {
  ctx(el)
  html` ...el content `

  ctx(portal)
  html`...portal content`
}

That's actually nice context switch. It removes need for update, destroy as well - turning current function into a current element renderer.

  • null-context, by the way, renders html to nowhere, which is nice too.

But besides switching context, there can be a task of listening selector.

Comparisons

Custom elements

MDN

// Create a class for the element
class PopUpInfo extends HTMLElement {
  constructor() {
    // Always call super first in constructor
    super();

    // Create a shadow root
    var shadow = this.attachShadow({mode: 'open'});

    // Create spans
    var wrapper = document.createElement('span');
    wrapper.setAttribute('class','wrapper');
    var icon = document.createElement('span');
    icon.setAttribute('class','icon');
    icon.setAttribute('tabindex', 0);
    var info = document.createElement('span');
    info.setAttribute('class','info');

    // Take attribute content and put it inside the info span
    var text = this.getAttribute('text');
    info.textContent = text;

    // Insert icon
    var imgUrl;
    if(this.hasAttribute('img')) {
      imgUrl = this.getAttribute('img');
    } else {
      imgUrl = 'img/default.png';
    }
    var img = document.createElement('img');
    img.src = imgUrl;
    icon.appendChild(img);

    // Create some CSS to apply to the shadow dom
    var style = document.createElement('style');

    style.textContent = '.wrapper {' +
                           'position: relative;' +
                        '}' +

                         '.info {' +
                            'font-size: 0.8rem;' +
                            'width: 200px;' +
                            'display: inline-block;' +
                            'border: 1px solid black;' +
                            'padding: 10px;' +
                            'background: white;' +
                            'border-radius: 10px;' +
                            'opacity: 0;' +
                            'transition: 0.6s all;' +
                            'position: absolute;' +
                            'bottom: 20px;' +
                            'left: 10px;' +
                            'z-index: 3;' +
                          '}' +

                          'img {' +
                            'width: 1.2rem' +
                          '}' +

                          '.icon:hover + .info, .icon:focus + .info {' +
                            'opacity: 1;' +
                          '}';

    // attach the created elements to the shadow dom

    shadow.appendChild(style);
    shadow.appendChild(wrapper);
    wrapper.appendChild(icon);
    wrapper.appendChild(info);
  }
}

// Define the new element
customElements.define('popup-info', PopUpInfo);
<popup-info img="img/alt.png" text="Your card validation code (CVC) is an extra
                                    security feature — it is the last 3 or 4 
                                    numbers on the back of your card.">

Spect

let $el = $('<div/>')
  .html`<${PopupInfo} img="img/alt.png" text="Your card validation code (CVC)
  is an extra security feature — it is the last 3 or 4 numbers on the
  back of your card."/>`

  function PopupInfo ($el) {
    $el.css`
      .wrapper {
        position: relative;
      }

      .info {
        font-size: 0.8rem;
        width: 200px;
        display: inline-block;
        border: 1px solid black;
        padding: 10px;
        background: white;
        border-radius: 10px;
        opacity: 0;
        transition: 0.6s all;
        position: absolute;
        bottom: 20px;
        left: 10px;
        z-index: 3;
      }

      img {
        width: 1.2rem
      }

      .icon:hover + .info, .icon:focus + .info {
        opacity: 1;
      }`;

    $el.html`<span.wrapper>
      <span.icon tabindex=0><img src=${ $el.img || 'img/default.png' }/></>
      <span.info>${ $el.text }</>
    </span>`
  }

Spect + maya

let $el = $('<div/>')

  .html`<${PopupInfo} img="img/alt.png" text="Your card validation code (CVC)
  is an extra security feature — it is the last 3 or 4 numbers on the
  back of your card."/>`

  console.log($el)

  function PopupInfo ($el) {
    $el.css`
      .icon:hover + .info, .icon:focus + .info {
        opacity: 1;
      }`;

    $el.html`<span.wrapper class=${maya`.p-rel`}>
      <span.icon tabindex=0><img class=${maya`w1.2rem`} src=${ $el.img || 'img/default.png' }/></>
      <span.info class=${maya`fs0.8 w25 dib b1solid p2 brad1 o0 t.6s pabs b3 l1 z3`}>${ $el.text }</>
    </span>`
  }

Collection types / live collection necessity

1. Selector observer

Always actual list or elements queried in the target

2. Dynamic selector

Queries each time ref is accessed or method call

3. Static selector

Creates NodeList once on first call, re-queries

4. Dynamic nodeList/array

Reruns for all elements whenever accessed or any method is called

5. Static nodeList/array

Runs only for elements defined on initial call, any further modifications of initial array are skipped

6. Single node

Attaches aspect to single node only

That is similar to swizzles.

  1. Since there are proxies in modern JS, the power of proxies should be used. The price is not the priority here.

Possible proxies:

  • $els[int] - get particular (wrapped? I guess not) element from the set.
  • $els['#id'] - get particular element from the set by id (confusion with privates?)
  • $els['.class'] - get els from the set, meeting the selector, so that is filter basically
    ~-> maybe $els.node[idx|id|selector] instead?
  • $els() - reevaluate maybe? Update?
  • $els.prop - prop, defined on selected set. Can be prop defined by other aspects or by html render, like <div prop=${val}></div>
  • $els.effect - call particular effect - domain connector
  • $els[symbol] - some technical methods?

~?! what if wrap elements/nodeLists with proxies instead? There's no props like html, css, use etc. - and that's direct correspondance to actual underneath elements. But although, these nodes aren't recognized by DOM, so.

store per-component / connection to external APIs

What if we'd store data per-component, but in generic storage?

function A() {
let [state, setState] = useStore('A.state', init)
}

function B() {
let [stateA, setStateA] = useStore('A.state')
}

+ simple useState API
+ localized and generalized data at the same time
+ no need for external store
- beware of recursion
+generic clean actions, not reducers

HTM algo

The algorithm of htm

Takes in htm tagged markup
outputs nested lists as:

htm`<main secondary secondary ...><main ...secondaries/>``
[main?, secondary, secondary, ..., [main, ...secondaries, children]]
  1. Then, the reconciliation takes in this list,
  2. Fetches cached real DOM aspect structure (if any)
  3. Finds out <...> tag
  4. ???

Method 1

  • The previous render had it's own vdom, now attached to real DOM
  • The new render has new vdom
  • We should identify set of operations to transform old vdom to the new vdom (level-like)
  • And apply these operations to real DOM

Side-effect: for cases when other aspects intrusively modified current aspect tree, that sequence of operations will keep these intrusions, not guaranteeing the structure provided by htm.

Too fragile.

Method 2

  • We figure out current vdom tree from real DOM
  • Identify operators needed to ensure new structure

no-js html init

MDL uses js-classes approach to initialize some behavior on pure html, like <button class="mdl-js-ripple"/>. Melding that with remount would enable react, initialized by classes.
Enabling that would allow to init mods directly on HTML without need for js.That would also enable bundle-free code.

It takes deciding though:

  1. how to install remote sources like react components?
    • the most standard way: npm install?
  2. how to import installed components?
    • import remote? import web_modules? import node_modules? import local files? standalone builds? bundling?
  3. how to connect components to classes autoinit?
    • Like remount - register? Registering custom elements?
  4. how to create new components?
    • Ideally we'd have function with hooks that renders custom element via htm

Documentation

  • Compare with frameworks (table, features)
  • List of app examples
  • Polished work-through intro
  • Docs is built with spect

CSS framework

Lightweight tachyons-like framework.

Generated online by defining set of preferences (theme). It generates standard radiuses, like no-radius, radius, pill; for typography - instead of set of font-sizes, it generates semantic variants. But for customization it also provides set of tachyons.

Ideas:

  • material-ui/evergreen-ui inspired
  • normalized API inconsistencies like b--white, bg-white, t-0, mt0
  • joined adjacent props like flex + flex-column, border-style + border-width
  • breakpoints as tailwindcss .s:prop .l:prop
  • less meaningless values like full css spec
  • linear 8px scale, like ui-box (designers can do math themselves)
  • traditional point-sizes names with some juicy scale vis
  • !pseudo classNames like :sm :hover .class:active
  • !set of standard components (tag, button, typographic) - with customized styles
  • !set of css effects
  • !customizable breakpoints/number/pseudos
  • !customizing shadows
  • ?keep props prefixed to indicate style group, no direct tokens. text-capitalize is better than capitalize. That is more intuitive. Direct tokens are for first-level components, or combinations of styles: .subtitle1, .button-primary.
    image
  • !ui-dashboard preview example https://twitter.com/jaukia/status/1134382045675868160
  • tachyons in classes make more sense than BEM: length of name is the same, just decomposed, but more flexibility, speed, descriptivity and customization, at not big price.
    ! call framework maya - literally matter, with molecules, atoms, tachyons
  • ! .theme-x (or alike) class name, redefining standard tags within with the preformatted default molecules theme, to ease up default look for programmers. Basically provides set of overrides.

Principles

  • don't override default DOM styles, just provide a toolkit of modifiers.
  • tachyons describe style props, atoms describe useful tachyon combos like flex-row-left, molecules describe primitives (tags-based, like hN, link etc.), themes provide particular look to molecules.
  • themed values are way less than exhaustive tachyons set: cool, warm, padding-big etc - semantic values, not low-level ones.

No selector observer

Nightly considerations.

Selector observer seems to be too contraversal for use: it observes both DOM and html effect - creates interference, it adds VDOM invalidation problem, it spreads project focus. Also that seems to be just a bit messy to track contextually. It is also tricky to understand.

For that purpose there is selector-observer package https://www.npmjs.com/package/selector-observer, or, more precisely, that concern can be separated, including dynamic results.

Instead, $ could be done more in jQuery fashion, querying elements instantly and providing effects context for them. That's easier to understand, easier to code/debug, provides better understanding of result. To bind all page components, that can be called in jquery-way, with document.onload.

Let's see in the morning.

Direct jsx

With spectators there's a subtle problem. Although it very reminds jquery and at all the idea of lifetime helper is really good, there's no clear way to mount things.
Should that be spect from the beginning to mount app? But then for apps that will be used just once - to start the app, and the rest is left to htm, which is way more important.

An idea, reminded by jsxify <document.body> and atomico <host> - putting spect into JSX to directly point to container <mount to=${target|selector} ${...attrs}></mount>. That enables portals, solves problem of passing attrs to host, instantly mounts app to possibly multiple parts of DOM and rids of JS in case of pure jsx.

htm`
<host portal='.dialogs'>
Dialogs portal
</host>
<host>
Root component
</host>
`

Possible approaches:

  • <root></root>
  • <host></host>
  • <div slot=${selector}></div>
  • <$${target}><//>
  • <_${target}></_>
  • <target></target>
  • <mount at=${selector}></mount>
  • <render to=${target}></render>
  • in, at
  • <${document.element}><//>
    + htm easter egg
  • <within target=${}></within>
  • <join with=${target}></join>
  • <attach to=${target}></attach>
  • <at target=${}></at>
  • <htm for=${target}></htm>.
  • <:.app><//>
    - confusing ending
  • <div container=${target}></div>
    + classical regl/component approach
    - should be a prop on every element in multiple branches, which is confusing, needs fragment, which would be <container=${target}></container> or just <${target}></>. Although - both of that is possible with htm lol, and even makes sense.

! <div.class#id></div> for shortcut

API

Effects.

  • $(target|el, handler) - query elements list in current context.
  • mod(selector, handler) - attach selector observer [within the current context].
  • html`...content` - make sure the content exists in the current context.
  • state(obj?) - set state values associated with the current context.
  • local - persisted state.
  • query - state reflected in history.

mod-style

Just do inline styles. Mods fetch them and distribute by tachyons.
Or you can pass list of tachyons:

<div m4 d2 />

html effect

Reconsiderations.

What is better?

If html applies content of current aspect as

el => {
// applies to current-target content
html`<div>content</div>`
html`<div><...></div>`
$('#button', el => html`other content`)
}
  • A bit confusing - the place content belongs to
  • No easy way to apply attributes to current target but mixing <${el}>, which opts for the next option. Or at least something like <self></self> attribute, which is meaningless since self-less html ensures content of current node anyways. Maybe we don't need setting attributes of self?
  • No way to create detached content but ensuring current content
  • Way nicer and simpler as side-effect.
    ~ Applying effect somewhere outside the current target is like $(sel, el => $(sel2, el => html``)), which is kind-of makes sense in terms of $ context provider. But then we stumble upon $ not having parent-subselector, so that it makes $ global-selector. We can introduce pseudos like $(':host sel', el => {}), but that doesn't solve the problem of sub-selecting elements like jquery $(sel, context?, el => {}). But considering there's no easy way to directly select elements (for subsequent parent-filtered selector), having :host pseudo is sane.
    ? what if we provide context via html.$(target)`content` ?
    . so basically context-dependent effects are like with (smth) { code }. To get access outside of current block, direct element should be indicated.
    . with(wrappedEl) { html }is the same as jquery'swrappedEl.html , so possibly wrapped.fx makes sense as API.
    ? what if we separate with effect as with(target, fx(...)), ? That's the same as $ though.
    . so maybe this one is ok, but the portals aren't much graceful. Mb portal is bad solution in general. Mb better to $(target, el => { $(otherTarget).html`` }) - actually pretty nice! Only we have to unbind children aspects whenever outer aspect is destroyed.

Or requires explicing setting of a target to apply content

el => {
// ensures target content
html`<${el}></>`
html`<${another}></>`

// returns new content
let frag = html`<div>content</>`
}
  • makes apparent containers
  • makes easier context-free html effect
  • makes sense of forwarding tags to render content to
  • makes possible returning/creating new html
  • allows multiple reducers in single html call <${a}><...></><${b}><...></>
  • makes reducer aspect dependent on the parent target, can be confusing what it belongs to.
  • element-tags may only make sense at root level, not somewhere within
    ? this method makes html a self-sufficient tool for ensuring content, even without selectors. Moreover, enabling selector-tags <#x></> <.something></> makes html a single-entry tool.
    • still that creates a problem of: should html ensure content or augment it. <...> allows reducing current target, with <${target}></> that's obscure: <${el}><a><b><...></b></a></> - is <...> - ${el} content or <b>?
  • technically, binding selector/element observer and ensuring html are two different operations, not good to mix them into single function.

Use-cases / ideas

CSS special classes handlers like

<div class="some-class@small other-class:hover"></div>

web-components as a standard components solution

See heresy for inspiration.
Also:

<nav-bar {...props}>
Children
</nav-bar>

Consequences:

  • we ought to have props list
  • there should not be predefined effects, all side-effects like css, html, title etc. are just hooks
  • shadow-dom for keeping effects scoped

components as hooks

Components are hooks, or reactions.
There are cases when hooks return JSX component and even require different rendering target.

  1. Render dialog into document.body - render into portal as effect. (usePortal hook). That is difficult with hooks.
  2. let Form = useForm(config, deps). Passing params into form component can be expensive - it does not require rerendering as often as root components' props change. It is also easier to configure form via params, since actual form template can be verbose.
  3. (conclusively) <A><B/></A> is just a form of effect - rendering some node into another node. That is perfectly covered with jsxify research - just a chunk of self-mounting JSX, matching DOM. As a compromise it provides mount(JSX, container) === <container>JSX</container>.
    mount(mount(content, subnode), node) -> document.createNode('div').appendChild(document.createElement(...)), so basically DOM tree is built top-bottom, not bottom-top as in JSX: h(el, {}, h(el, props))

Losing scope in async global effects

Consider aspect

import $, {state} from 'spect'

$(el, el => {
setTimeout(() => state({x: 1}))
})

In such case, state is called in separate tick, therefore loses currentTarget and is hard to identify the aspect to rerender.

Similar situation happens with async functions, where fn continues in a separate tick, dropping the callstack. That is fixable in latest node https://thecodebarbarian.com/async-stack-traces-in-node-js-12, but impossible anywhere else.

Approaches:

Parsing callstack

Possible approach is storing aspect bodies by callsite, and figuring out "out of tick" effect calls based on if their initial callsite is contained within one of tracked aspects.
That is purely visual approach to code. It is slow-ish, non-standard-ish (stacktrace is not regulated feature) and with medium weight logic. Besides, bound or native or alike aspects, which are not serializable aspect.toString(), instantly lose source (toString gives [native code]). We can hoist up, looking for nearest triggered aspect entry, presuming that is the source of async call. But that breaks trivially when we delegate aspect handling to some external function, that can reside in a separate module.

So that can work with some limitations, similar to react hooks.

  1. The effect should be called visually from the same scope as the aspect. Effects residing literally anywhere outside of effect scope aren't going to work (until browsers learn --async-stacktrace).
  • But - that wouldn't even work with visible arguments. If setTimeout callback is something external, that's naturally out of reach.
  1. Effects cannot be incorporated into wrappers. (Wrapping effects must be registered).
  • As far as wrapper is called from aspect scope, it is detectable.
  1. Aspect functions cannot be external/native. They must be unbound, described in-place, with valid toSource.

Out of scope effects can be figured out and error thrown.

Static transform

That is likely must-do, turning generic effects into particular ones, mb wrapping them with bind.

Webworker sandboxing

Another form is creating a webworker per aspect, that would also solve the #38. Tool like https://github.com/GoogleChromeLabs/clooney/ can be to the place.
That creates a web-worker aspect, but the created sandbox loses access to surrounding environment. Besides, creating a sandbox per-aspect scope is quite heavy.
That's better left for a separate effect, eg. work, on the stage of web-components:

el => {
  let result = work(() => {
  })
}

See #38 for progress on that.

Possibly like VM, that could reproduce global context, although would require same imports as the main one... Seems like breaking fx rules isn't good idea.

Runtime sandboxing

There seems to be no other way but create a runtime sandbox, wrapping initial aspect source with bound effects

$(target, el => {
html``
})

// converted to
$(target, ((html, fx, ...) => { return (el => {
}) })(html.bind(target), fx.bind(target))  )

That's going to be the fastest and safest within others, considering that the code is just wrapped, no user input expected (life can be ironic though).
Problem with this approach is that effects referred by the aspect can have any aliased name within the current module.

So this can be used with limitations.

  • Imported effect names. Similar to limitation on react hook names.
  • The error can be displayed to avoid aliasing effect names.
  • Fn must have toSource, there's no way to wrap that keeping clean source.

VM sandboxing

Running aspect code in a separate context. In browser that's done via iframe.
It's not necessary to replace globals. We have to mock deps per-aspect.
Sounds horrendous of course, although mb possible.

Throwing/catching error to obtain async stack

The error thrown in setTimeout is thrown in another tick. It may have no access to the original scope. Even without global effects. Therefore the effects should be created by scope in this or another way, but if setTimeout intends to run it outside of the scope, there's no way to do it even in react. Same is for external functions - if an effect is run outside of scope, it naturally has no lexical access to the source scope. That isn't even possible with jquery-like refs. To have access to scope the aspect arguments must be lexically visible.

Transition from use* to jquery

Say, we have use- lib as:

import { useAspect, useRoute, useAttr, useState, useHtml, useEffect } from 'spect'
import { t, useLocale } from 'ttag'
import ky from 'ky'

// main aspect
function app (el) {
  useAspect(preloader)
  let [ match, { id } ] = useRoute('user/:id')
  let attr = useAttr({ loading: false })
  let state = useState({ user: null })
  let html = useHtml()
  
  useEffect(async () => {
    attr.loading = true
    state.user = await ky.get(`/api/user/${id}`)
    attr.loading = false
  }, id)

  html`<div use=${i18n}>Hello, ${ state.user.name }!</div>`
})

// i18n aspect
function i18n (el) {
  let { lang } = useAttr(document.documentElement)
  useLocale(lang)
  let html = useHtml()
  
  html`${ t`${ el.textContent }` }`
}

// preloader aspect
function preloader (el) {
  let { loading } = useAttr()
  let html = useHtml()
  
  if (loading) html`${ el.childNodes } <canvas class="spinner" />`
})

// attach aspects to target
useAspect(document.querySelector('#app'), app)

So the pattern is let method = useSomething(target?, init).

Suppose we join items into a single use scope:

import use from 'spect'

// main aspect
function app (el) {
  use.aspect(el, preloader)
  let [ match, { id } ] = use.route('user/:id')
  let attr = use.attr(el, { loading: false })
  let state = use.state(el, { user: null })
  let html = use.html(el)
  
  use.fx(async () => {
    attr.loading = true
    state.user = await ky.get(`/api/user/${id}`)
    attr.loading = false
  }, id)

  html`<div use=${i18n}>Hello, ${ state.user.name }!</div>`
})

Ok, we can collapse multiple use.*(el) into a single use(el):

import use from 'spect'

// main aspect
function app (el, use) {
  use.aspect(preloader)
  let [ match, { id } ] = use.route('user/:id')
  let attr = use.attr({ loading: false })
  let state = use.state({ user: null })
  let html = use.html()
  
  use.effect(async () => {
    attr.loading = true
    state.user = await ky.get(`/api/user/${id}`)
    attr.loading = false
  }, id)

  html`<div use=${i18n}>Hello, ${ state.user.name }!</div>`
})
...

Merging use and el we get $el:

import $ from 'spect'

// main aspect
function app ($el) {
  let [ match, { id } ] = $el.route('user/:id') // we need $el here to attach updating el effects
  
  if (!match) return
 
  $el.fx(async () => {
    $el.loading = true
    $el.user = await ky.get(`/api/user/${id}`)
    $el.loading = false
  }, id)

  $el.fx(preloader, $el.loading)

  $el.html`<div fx=${i18n}>Hello, ${ $el.user.name }!</div>`
})
...

$(el).fx(app)

Sync vs async effects

// RESEARCH:
// should fx be synchronous or async?
// + async fx is similar to react
// - sync fx is more natural, and is easy to control by appending async param

// should state({}) set state synchronously or async, in the next tick?
// + having state persistent for the current render, and rerendering if it's changed after the render saves computation
// but that doesn't justify a separate tick
// for optimization purposes effects may be trigger their logic not hardly bound to the aspect tick
// for example, html rerender may trigger after state is reconciled and new frame is reached.
// although it may trigger instantly.

Let's make all effects sync in the first run and see.

Better fx deps

React useEffect is pretty poor on deps handling.
fx should user-friendlier compare deps values, as deepEqual.

Aspect as class

Aspect as class would group many separate fx into a single holder.

  • current flag
  • observables set
  • dirty flag
  • state holder, although state is bound per-target, but that could be shared between aspects
  • fx count and associated deps
  • .call method, so that we can put aspects directly into queue
  • vdom for planned html to render on next raf
  • constructor would be able to return existing instance to avoid reuse
    ? $el in use($el => {}) would be able to provide actually an aspect instance $el, opposed to wrap?
$app.use($el => {
$el !== $app // ? is that ok
})
  • it could store parent aspect
  • it could provide web-component / aspect types

Features / gems / buzzphrases

  • separate aspects: auth, vis, sound, logging, meta, messaging etc.
  • vanilla-first
  • Respects semantic HTML
  • web-components friendly
  • clean tree
  • Particles of behavior, pieces / fragments / atoms / particles of logic
  • Can be gradually infused into react/JSX, reducing tree complexity
  • Replacement to HOCs
  • Natural hydration
  • 0 bundling
  • real SPA
  • best of react, rxjs and jquery worlds

mod-style

Inspiration: tachyons, ui-box/material-ui box, modifiers, emmet.
Tiny particles taking values applying visual transforms to elements.

  • not JS style handling

Context-dependent effects, or jquery-spect

Trouble of async fx brought a situation, involving related problems.

The problem:

import { html } from 'spect'

fx(async () => {
await x

// at this point we have lost parent's currentTarget
// and seems there's no way to figure out the scope from which the `html` is called
html`...content`
})

There's another point - importing all possible effects each time they're needed is tedious (imagine jquery had to import all effects whenever wanted to use them).

import $, { fx, prop, attr } from 'spect'
...
$(target, el => {
prop()
attr()
fx()
html``
})

// vs tape-like entry
$(target, (el, {html, prop, attr, fx}) => {
html``
fx()
attr()
prop()
})
  • This way deps are context-dependent, which makes them more apparent.
  • This way no need to scroll up to import effects
  • This way effects can be async
  • This way the problem of providing context via $.call is solved naturally
  • This way $.fn makes sense for registering effects
  • The drawback is repetition of imports each $ / not very graceful destructuring

Is there a graceful way to provide contextful effects?

Fx as second argument (el, {html}) => {}

React-like el as props, this has effects (el) => { this.html }

  • react-compatible props
  • hidden effects - obscure

Tape-like argument with effects, this is element $ => { $.html, this === el }

  • tape-compatible
  • jquery-compatible
  • event-handler compatible
  • react-incompatible props argument
  • available $.fn

Wrapped object, jQuery-like $el => { $el.html }

  • jquery-compatible and familiar, but better API
  • enables direct modifiers $(selector).html
  • not as elegant as react destructuring
  • $el => $el.html is better indicator than (el, $) => $.html

!?!?!?! What if we just extend jQuery with react hooks !?!?!?!?!?!?!

naah. state is very nice to be bound to aspect, not to element $el.state(), although tempting. route has nothing to do with jQuery. $el.fx(() => {}, [id]). $el.html.... That indicates apparent context of actions, which is good. $(el, el => {}) is impossible, it is $(sel).spect($el => {...}) now, because it requires re-rendering, hence to be called again.

  • we keep jquery as familiar to everyone default
  • we support huge infrastructure of plugins
  • we bring hooks/aspects as integral part
  • solves problem of context-dependent calls and all abovementioned
  • a bunch of effects is solved - on, css.
  • may need extending html though and related fns, possibly as a separate plugins
  • may be overhead for small focused vanilla projects.
  • spoils lightweight spirit of effects.
spect(el, $el => {
let el = this // el = $el[0] like jquery
$el.html
$el.on()
$el...
})

spect(el, ([el]) => {})
spect(el, ({0: el, html, ...$el}) => {})

~ we can separate to jquery-spect plugin, enabling aspects for jquery. But that brings some effects compatibility syntax - we force on effect be fully compatible with jquery, although we can automatically off them when the aspect is removed. Also that anyways extends standard jquery html effect to accept vdom stuff.

Of course that's freaking mad connecting react to jquery as $el.react(JSX).

On effect

<div on=${ 'evt1 evt2': handler }/>

<div on-evt1-evt2=${handler} />

<div on="evt1 evt2 ${handler}" />

<div on=${[evt1, evt2, handler]} />

Safe web components library

We need a library of distributed components from the web. Isolated, atomic.
Such as: popover.

htm`<a href... popover="url"></a>`

Callstack mechanism

One container may have an [ordered] set of targets. If some target already exists, we reuse that. Initial run creates order.

One target may have an [ordered] set of aspects. If some aspect already exists, we reuse that. Initial run creates order.

One aspect may have an [ordered] set of effects. If some effect already exists, we reuse that. Initial run creates order.

[?] mb better [ordered] replace with [sequence] - that can be nested, hierarchical for example. That order should be persistent.
One effect may have an [ordered] set of sub-effects. If some sub-effect already exists, we reuse that. Initial run creates order.

Mod-react

Better have mod framework entry instead of react.
It's easier to integrate react into mod than vice-versa.
Components are mods, not components have mods:

<div mod-react=[composable, react, sequence]/>

<div mod-react>{<Components>}</div>

Hooks redesigned

  • let [x, set] = state
    • local "store" is unseparable from update
      • that is addressed via useRef, that is super-confusing
    • oftentimes we see "fake" useState, let [,update] = useState() just to force rerender
    • we need just "local state" and/or "update"
    • if we use multiple set states in order, they trigger one after the other
  • useEffect(() => { init; update; return destroy; }, deps)
    • scary name indeed
    • so much confusion using it: no clear understanding when it's been triggered or why useEffect(fn, []) !== useEffect(fn)
    • no definite indicator of triggering, sometimes it depends on forced "fake" id from useState, just to re-trigger it
    • no returning result
    • any attributes change cause retriggering destroy
    • mount/unmount are more apparent, although don't have shared state
    • change by condition is more apparent
    • conceptually there is
      • let shoot = load(() => {}), which can be async too
      • updateDiff(fn, deps), similar to update-diff
  • useContext, useRef, useReducer are just ... react legacy stuff
  • useMemo
    • because of signature conflict with useState, it requires redundant functional wrapper, could be let result = memo(() => {})
    • intuitively very similar to useEffect, but takes no deps list

Prepend and other html manipulations

How to prepend some content?

As a modifier
$`<aspect sel="some-selector" prepend=${content}/>` -adds functional-ish aspect -not-position-based

As a content placeholder
$`<aspect sel=".some-selector">${content}<parentNode.children></>` - prop access in tagname

Direct way
$`<aspect sel=".some-selector">${content}${el => el.childNodes}</>` - breaks html

HTML-way
<aspect sel=".some-selector"><i></i><slot children/></aspect> + logically meaningful slot application

react/vdom rendering drawbacks

  • single-direction of props. With usual components we can pass props and then read component's state to reuse changes. With react we can't read component's state, since that's isolated.
  • Lost queries: inferring the data from the DOM. With simple queries that's easy to just get all data, with react that's impossible.

Lifecycle hooks

1.Should that be a counstructor instant run with registering fx ⭐

$(div, el => {
  // ~~constructor~~
  // render, since reruns any time any hook changes state
  let [state, set] = useState()

  // hooks
  mount(()=>{})
  update(()=>{})
  unmount(()=>{})

  // ~~detructor~~ never run
  return () => {}
})
  • a bit more flexibility in following classes paradigm (not sure how much is that a merit)
  • allows returning values
    easier to separate aspects, eg. put rendering in a separate aspect. Why though?
  • easier to condition advices via deps
    • no need to condition mount/unmount
  • closer to real AOP with pointcut-advice
    • in most cases that is run on mount/unmount anyways
  • technically can be merged with 2. and treat main fn as constructor and returned fn as destructor
    • not clear though when to call destructor lol. window.unload?
  • no way to define static props, the constructor is called per-element instance in DOM.
    • define them outside of aspect
  • an easier way to register all required listeners
    $(target, el => {
    let [a, b, c] = attr('a', 'b', 'c')
    update(fn, [a, b])
    })
    • ok, we read attributes, but the update - what deps should we pass to it to indicate what to observe? Should a,b,c be sort of proxy objects? Or again - primitive wrappers? Confusing!
      • Not necessary! Attributes can be just values, that we can parse later. Essentially it can just register listeners for attributes, that's it, so whenever attributes change, main method runs, that's it.
      • Maybe not necessarily that should be that bad. Other hooks can save situation, like state - so that the main method is run any time state is set.
  • clearer terminology from AOP
    ~ possibly less complacency with vanilla packages, alhough... constructor once seems what they're doing
    ~ doubtful situation if we init element anywhere else but mount.
  • inconsistency with selectors: for selectors this is run always on mount.
  • can be confusing to have aspect run for unmounted element, possibly better delegate unmounts to direct functions, since access to them is present
    . main method is not constructor. It is "render". An it is run whenever observable state changes.
    • that leads to 2.
      . constructor is used to init data placeholders, mostly it.
      ? how about aspect of offline canvas?

2. or fx hook straight ahead

$(div, el => {
  // mount or update
  return () => { /* destroy */ }
})
  • 1st-class shared state, no redundant scoping
  • no off-react conventions, more react-friendly
  • possibly easier to handle useEffect:
$(target, el => {
let x = attr('x')
useEffect(() => {}, [x])
})
  • although that observes all attribute changes
    • we anyways have to observe all attributes and subfilter some
    • we can register observers on mount - no sense to observe unmounted components
  • augmentor-compatible
  • that seems to be like hooks inside of element-effect hook.
  • no way to make sense of returned value or to have multiple returned values
    ? how to init stuff in unmounted component? Eg. custom element constructor?
    ? need we? What if we ignore off-mount components? $('selector') is anyways always affects real elements, can't init on unmounted elements.
  • weird aspect - that ignores element until it is mounted.
$(div, el => {
  // called only on mount - confusing!
})

The dispute seems to be returned unmount callback vs fulltime aspect.
The winner is 1. - that keeps promise for non-dom aspects, as well as enables unmounted elements. Also that makes lifecycle pointcuts more apparent.

Suspense

$(document.body, import('./external.html'))

let result = $('.app', async (el) => {
  $(el, 'Loading...')  

  let result = await someQuery
  
  $(el, result)

  return result
})

To no - rejected APIs

Some rejected API.

1. jQuery-like methods
$(target).html() - violates standard element results, doesn't bring much use over $(target, el => html()).

2. fx as attributes
<div html="" fx="${fn}" /> - pollutes attr namespace, no much use compared to direct aspects.

  1. Empty selector case
    $(el => {}) - as if function is a target or selector, which is not.

Props as direct effects

Which option is right?

html`<${Cmp} foo bar=${'baz'} qux=${fn}></>`

function Cmp($el) {
// 1.
// $el.foo

// 2.
// $el.prop.foo

// 3.
// $el.attr.foo
}

1.

  • react-compatible
    ~ namespace conflict with effects: html, text, css, use - can't redefine that.
    ~ although, mb that's bad idea anyways to create clashing props $el.prop.css
    ~ also, mb that's good idea to define effects as props <div html=${}> - skips aspects step.
  • we could get rid of prop and attr effects this way, keeping everything as prop, just prop namespace is limited
    ~ standard props are like effects too <img src="..."/>
  • almost like direct el.src (vs $el.attr.src)
    ~ we can disable standard effects if main component is passed (moreover likely we should):
    <${PopupInfo} text=${'Error happened: no such thing'} /> - passes text prop; <div text=${123}/> - sets text effect.
    ~ we can provide super effect, invoking standard aspect for props as $el => $el.super()

Friday demo

  • Move to org

  • Proofread readme

  • Basic effects set: on, css, class, data, init, mount

  • React examples

  • Standalone build/publish

  • Admin portal

  • Components

  • React comparison

  • Benchmark

  • FAQ: frameworks, integrations, debug, maintenance, security

  • Working examples

  • Basic react-compatible API

Anonymous aspects: for and against

  • From the examples there are not many use-cases when it's super-required not as primary aspect
  • It makes difficult to have handwritten JSX notation (jsx incompat), as well as bad for compiling to hyperhtml.
  • Component === anonymous main aspect, so the issues is when secondary anonymous aspects are profitable?
  • Considering that aspects are applied to the full DOM, some of them can be off-component,
  • Order of attribs is html-incompatible concept.
    ? authentication
    ? connection to store
  • html`<${el}`/> or $(el, fn) allows extending elements with additional aspects.

It just supposes all secondary aspects should be named, that's it. All primary aspects are implicitly named, since we describe main reality. <div something> literally means <tag=div something>, so anonymous aspect there doesn't point what reality that belongs to.

Any enumerable aspect can be taken as a base for reality, eg.

// <disabled=true>
<true tag=a/>
<true tag=b/>

tooltip mod

// react
<Tooltip label="tooltip content">
  Actual content
</Tooltip>

// 3A
<span title="Tooltip content" mod-tooltip>Actual content</span>

Aspect indicators in HTML/JSX

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?

  • Should be hydratable - identifier with serialized params
  • Should enable simple insertions via htm
  • Should be elegant and natural

1. Class/Id tokens

h`
<div.text-input.autosize ...>
<div.date-range-input ...>
<div#grid ...>
`
  • lucky mix of hyperscript / aspects
  • only classes/ids are allowed for aspect selectors anyways (enabling container prop)
  • duplicate definition of classes <div.date-range class="date-range" ...>
    . id/class aspect difference is uniqueness of aspect on the page
  • easier extension/modification
  • known h/query- syntax
  • aspects need to be registered by names
  • no way for multiline tagname if too many aspects
  • possibly stylistically mixable with portals as <#portal><div#aspect/><//>
  • looks unlike html

2. Direct component

h`
<${[TextInput, Autosize]} ...>
<${DateRangeInput} ...>
<${Grid} ...>
`
  • standard JSX/html approach
  • noisy code highlight
    ~ seems to be unavoidable pattern by htm, possibly can be prohibited

3. is, use attributes

h`
<div use=${[TextInput, AutoSize]} ...>
<div use="date-range-input" ...>
<div is=${Grid} ...>
`
  • standard syntax
  • possible extension of 1., when classes are not applied (noname aspects)
    ? what's the meaning of direct components then? the same as is.
  • has to use html tags for every here and there.

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?

  • aspects need to be registered as custom elements, but aspects are not always custom elements
  • possibly custom elements can be registered via some

5. @ attribute

h`
<div @text-input @autosize ...>
<div @${DateRange} ...>
<@${DataGrid} ...>
`
  • html decorators
  • @ meaning @spect.
  • @ is not valid attribute name, no native html support

6. @-prefixed classes

h`
<div class="@text-input @autosize ..." ...>
<div class="@DateRange" ...>
<${Grid} ...> 
`
  • compatible with custom css classes generaded by styled-like systems
  • obvious indicator of different nature class
  • 5.: decorator, @-meaning
  • safe undestructive classname. Really, super-safe.
    ~ <@ is not valid tag name
    ~ possibly have to register all aspects by token name
  • titlecase-names allow direct class names
    • no way to convert @-names to functions

7. Generated classes

h`
<div class="${TextInput} ${Autosize}" ...>
<div class=${DateRange} ...>
<div id=${Grid} ...>
`
  • least intrusive, very safe
  • no naming headache
  • more apparent than use, since it's clear that this is a string
  • no @- noise
    ~ custom elements are kept aside, possibly a target case
  • makes sense for registering . and # selectors
  • $ in h corresponds to $ outside.
    ~ @ can be used as generated classes prefix.

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" ...>
`
  • no need to care about selectors, registering aspects: anything defined in <@> tag is treated as aspect, anything used via @ class applies aspect
  • @ is not allowed html element name
  • confusing aspect definition and usage: no way to do first-level aspect as element <@Grid. We're off custom elements and components.
  • @ class is actually <.@TargetName aspect by classes.

9. Attributes

$`
<div text-input=${params} autosize=${params} ...>
<div date-range=${params} ...>
<div grid=${} ...>
`

Questions / ideas

Previous proves are scattered in various issues #44 #43 etc.

Here are the ideas / new ones. Each prove has own reply.
Large ones can be addressed in separate issues.

Name

  • useful
  • treac
  • 3A
    • sounds like t-reac
    • A3 from aspect-oriented programming
    • reference to trimurti - creation, maintenance, destruction, the stages of any life being
  • aaa
  • mods (taken mostly, only npm org is available, in js even mod-js is taken)
  • dom-fx
  • e-m
  • hyperfx
  • hfx, like hui
  • hyperhooks
  • modx
  • hmod
  • hyfx
  • hfx
  • fxs, fx-sheet, fxsheet

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.