jorgebucaran / superfine

Absolutely minimal view layer for building web interfaces
https://git.io/super
MIT License
1.6k stars 79 forks source link

What if: class components? #135

Closed jorgebucaran closed 6 years ago

jorgebucaran commented 6 years ago

About class components (https://github.com/jorgebucaran/superfine/issues/134), I was thinking, what if we could do something like this?

import { h, Super, patch } from "superfine"

class Hi extends Super {
  state = { say: "Hi" }

  render = () => (
    <h1>{this.say}</h1>
  )
}

patch(null, <Hi />, document.body)

Just throwing crazy ideas out there?

rbiggs commented 6 years ago

What would Super be? Almost starts looking like PreactLite, which isn't necessarily a bad thing.

zaceno commented 6 years ago

The question with class-based components with local state is how you can share the components state with children. Like:

<Hi>
   <Child1 />
   <Child2 />
</Hi>

... how can the internal state of that Hi instance affect the children?

Of course, if you don't want to do that it's not a problem :P but as far as I've understood React (which is not much, tbh), stateful components would be almost useless if not for the context api. So perhaps you should consider adding that too?

Fwiw, that's why I avoided class-based stateful components in zxdom. Rather than instantiate components when they appear in the view, I allow already instantiated "widgets" (sort of equivalent to stateful components) in the vdom. So all the wiring between widgets is done before they go in a vdom, which means I don't need a context-api

zaceno commented 6 years ago

Actually I take it back. The way it's done with custom-elements, and could be just as useful in stateful components, is with events:

Some parent (Parent) component could be the one composing children under the Hi component above. When something relevant in the internal state happens in Hi, it can trigger an event (regular dispatchEvent(new Event('something')))

Now Parent can be listening for onsomething, and update Child1 and Child2 accordingly.

mindplay-dk commented 6 years ago

@jorgebucaran I'm actually still with (past?) you on this one - classes/constructors are not the best approach to stateful components.

React is increasingly moving away from instance life-cycle methods (for various reasons) towards static class life-cycle methods, or in other words, a bunch of functions that don't have anything to do with the component instance and only with the component type.

This is why I started talking about a service-oriented approach - so like:

const Hi = {
    init: props => ({ say: `Hi, ${props.name}` }),

    render: (state) => (
        <h1>{ state.say }</h1>
    ),

    remove(element, state) {
        // ...
    },

    destroy(element, state) {
        // ...
    }
};

patch(null, <Hi />, document.body)

In other words, move away from components as instances, to components as some sort of service interface responsible for each of the life-cycle phases of component state - everything becomes a pure function, and, especially, the render-function becomes a pure function of the state.

I haven't thought through this, but as long as we're just kicking around ideas, method-props would probably allow you to write event-handlers that are pure functions as well - which would be a big step up from P/React where you always need to create and initialize instance-properties in the constructor to hold event-handlers, to avoid generating new function-instances for every component...

mindplay-dk commented 6 years ago

To explain that last bit... in P/React:

class Button extends Component {
  constructor() {
    this.onClick = (event) => {
      // ...
    };
  }

  render() {
    return (
      <button onClick={this.onClick}>{this.props.label}</button>
    );
  }
}

As opposed to something like:

const Button = {
  onClick(event) {
    // ...
  },

  render: (props, state) => (
    <button onClick={this.onClick}>{props.label}</button>
  )
}

As said, I have not thought this through at all, it's just a half-baked idea...

mindplay-dk commented 6 years ago

On that note, I've also been wondering why we need the separation between props and state at all.

Typically, I end up with a private accept method and some boilerplate in my components:

class DatePicker extends Component {
  constructor(props) {
    this.accept(props);
  }

  private accept(props) {
    if (props.value !== this.state.value) {
      this.setState({ value });
    }
  }

  componentWillReceiveProps(props) {
    this.accept(props);
  }

  render() {
    // ...
  }
}

It's a really ceremonious way to derive state from changes to props - you might as well call it all "state", have a single object and derive everything by accepting changes to props:

const DatePicker = {
  init: props => props,

  accept: newProps => newProps,

  render: (state) => (
    // ...
  )
}

In other words, the lib diff changes to props, just like we do for HTML elements - this should bring things closer inline with the way regular DOM elements (and possibly Custom Elements) are implemented.

We'll call accept only if any props have changes since the last render. In this particular example, the initial state is equal to props, so init: props => props, and, as it happens, for this component, any updated props, we just want to apply them over the current state, so accept: newProps => newProps and it just accepts everything. Maybe the lib merges the returned new state over existing state, or maybe the signature is (newProps, oldState) => newState which might provide more control, not sure. (this example is incomplete, and most components would probably check which props were updated, and deal with the changes in whatever way is meaningful to the component, derive new state, etc.)

The x => x functions could possibly even be the default for init and accept - just accept all initial props as state, and accept all updates as new state. (This wouldn't quite be the equivalent of a functional component, since in addition to initial/derived state, event-handlers could inject additional state values.)

acstll commented 6 years ago

Never used this but it could be a source of inspiration, I loved the fact that it wasn't classes, you didn't need to extend anything: https://mithril.js.org/components.html

zaceno commented 6 years ago

Since we're talking about stateful component API's I thought I'd share how I do it in zxdom, because it's a different twist (I think). While multiple instances of a widget/view/component in zxdom can be made to behave differently with props, they are really mirrors of the same state.

A counter component might look like this:

import {define, update} from 'zxdom'
var value = 0
function up () { 
  value++
  update(Counter)
}
function down() {
  value--
  update(Counter)
}

export default const Counter = define(props => (
  <p>
    <button onclick={down}>-</button>
    {Math.min(props.max, value)}
    <button onclick={up}>+</button>
  </p>
))

Now, crucially, and different to React, this:

<main>
  <h1>Counters:</h1>
  <Counter />
  <Counter />
</main>

... this will not produce two separately functioning counter instances. All of their buttons affect the same state value in the counter module.

However, they can be made to behave differently with props.

This:

<main>
  <h1>Counters:</h1>
  <Counter max={10} />
  <Counter max={100} />
</main>

As you keep clicking either + button, both will keep increasing, until 10, where the first one stops, while the second one can keep going until 100.

zaceno commented 6 years ago

Also, maybe it would be better to encourage custom elements, rather than stateful components. Here's a superfine counter-input custom element:

const { h, render} = superfine

class CounterInput extends HTMLElement{
    constructor() {
        super();
        this._value = this.getAttribute('start')
        this.update()
    }
    view () {
        return h("p", {}, [
            h("button", { onclick: () => this.decrement() }, "-"),
            this.value,
            h("button", { onclick: () => this.increment() }, "+")
        ])
    }
    update () {
        const newnode = this.view()
        render(this._lastNode, newnode, this)
        this._lastNode = newnode
    }
    increment () { this.value++ }
    decrement () { this.value-- }
    get value () { return this._value }
    set value (x) {
        if (this._value === x) return
        this._value = x
        this.update()
        this.dispatchEvent(new Event('change'))
    }
}
customElements.define('counter-input', CounterInput);

And here's a demo of it (being used by vanilla js, but it could just as well be used as a vnode in a superfine-app, or anything else):

https://codepen.io/zaceno/pen/wXLWdp?editors=1000

(Polyfill not included, so only works in chrome and safari atm)

mindplay-dk commented 6 years ago

@zaceno I also experimented with custom elements here - I think they will be useful for some things, and my hope was we wouldn't need a component model at all, we'd just implement custom elements, which this library already supports.

I abandoned that idea when I realized the custom elements API is modeled pretty accurately like DOM, where, unfortunately, updates cannot be applied as a set. That is, you have methods (and/or getters/setters) and setAttribute, which allow only property-by-property updates. This is actually real problem - for example, imagine you have a custom numeric input with min/max values:

// first update cycle:

el.max = 100;
el.min = 0;
el.value = 50;

// second update cycle:

el.max = 10; // value of 50 is invalid now!
el.min = 0;
el.value = 0; // value is valid again

With atomic property-updates, there will be moments where the component is internally in an invalid state, and the implementer has to decide what to do: allow a value outside the permitted range? clipping the value and lose the value you were trying to apply would mean the resulting state of the component is not what you were trying to apply. You can't know at the time when the value-setter gets called if there's another update of the min/max settings on the way. This creates complexity.

Compare with e.g. P/React, where this update is atomic and performed during a single life-cycle callback - the new props/state are in a complete, valid state, all the time, never in this weird "limbo" state where they may be internally invalid according to their own specs, never creating these scenarios where you need to decide how to deal with temporary invalid state during transitions.

Custom elements are kind of wonky in this way - I don't think they can really replace a component model.

jorgebucaran commented 6 years ago

@mindplay-dk Obviously you will implement custom elements using Hyperapp or Superfine.

jorgebucaran commented 6 years ago

I created this issue to gather feedback about class components, that's all. I'm the least excited about class components, but I thought there may be something interesting to say about them.

It looks like we're done here.

mindplay-dk commented 6 years ago

For the record, now that I have to live with them on a large project on a daily basis, I am also not excited about class-components anymore - but I think there must be a better way, and I'll keep experimenting.

Either way, I think this is definitely outside the scope of the library ;-)

mindplay-dk commented 6 years ago

If you don't mind, I'd like to continue exploring the idea here.

I've been trying to hack something into the library, and I think I realize now where the roadblock is - it's in the end of the h function, where the expansion of functional components happens "too soon" and provides too little information to implement any sort of component concept, I think.

  return typeof name === "function"
    ? name(props, (props.children = children))
    : createVNode(name, props, children, null, props.key, IS_VNODE)

Expanding functional components at this level, means we miss the opportunity to diff the props of the functional component itself - by the time patch gets called, we're only diffing the nodes that the functional component produces.

I think we need to change the definition of VNodes, so that what's currently the name property may also be a function that we delegate to at a lower level to produce the actual nodes.

In order to implement any sort of component model, functional components need access to lastNode, where currently they only have access to nextNode, which doesn't allow any kind of reconciliation within the function itself - it can't reason about it's last state compared to it's new state.

@jorgebucaran thoughts?

jorgebucaran commented 6 years ago

@mindplay-dk Yes, we unwrap components eagerly. I experimented with something I called "lazy components", which is a V1 feature, but it's going away in V2 (you can already guess where this is going).

Resolving nodes during diffing is how stateful components can be implemented. It's also one way to implement https://github.com/hyperapp/hyperapp/issues/721, but I am looking for an alternative solution to that as I want to continue evaluating components eagerly.

One problem I encountered with node resolution while diffing is how to deal with components that return null.

mindplay-dk commented 6 years ago

I am looking for an alternative solution to that as I want to continue evaluating components eagerly

I don't understand how that would be possible, even in theory?

The problem is, JSX compiles to nested h calls, which evaluate from the inside-out - because the compiled output is nested expressions, the edge node expressions are evaluated before their parent nodes, so if you evaluate eagerly, you're evaluating from the inside-out.

Components need to run to decide what happens to their children - so you can evaluate edges-to-roots only while processing actual DOM elements; when you encounter a component, you essentially need to treat it as a new root of an independent patch-operation, I think? Not sure...

One problem I encountered with node resolution while diffing is how to deal with components that return null.

Yeah, that one boggles me too ;-)

jorgebucaran commented 6 years ago

@mindplay-dk Yeah, that's exactly right. Still, you can implement lazy lists (hyperapp/hyperapp#721) without making name optionally a function. It does require adding some new props to the VNode constructor.

mindplay-dk commented 6 years ago

@jorgebucaran right, so then I don't think there's any way around a two-stage process of some sort?

So I think functional components need a preliminary representation as a VNode where name is a function.

Then, before we start processing a list of children, we need to scan that list of children for VNodes representing functional components and invoke them - then either:

a) splice the resulting VNodes (zero or many) directly into the list of children where the original functional VNode was located, or

b) replace the resulting VNodes (zero/one/many/null) in the list of children, and perform the folding of nested arrays (which happens in h now) in one of the lower layers

FWIW, Preact appears to do something similar - it's h function doesn't immediately run neither functional components nor component constructors.

jorgebucaran commented 6 years ago

@mindplay-dk They support stateful components and vnodes are allowed to be different things: strings for TextNodes and instances/vnodes for everything else. I'm not a preact expert, you'd have to ask Jason.

Now, I'm exploring different options to implement lazy lists for us and the one I have in mind right now is adding a new lazy field to the VNode without modifying how h currently works.

mindplay-dk commented 6 years ago

They support stateful components

Sure, but stateful components (constructor references) are also just functions - they happen to produce a component instance, and we're not interested in that, but the problem is otherwise the same: functional/stateful components do not directly represent elements, they represent something that can produce elements, but you can't know what they produce until they run.

Preact stores everything in the DOM element, btw - it injects up to four different properties into every element - and performs just fine. Just saying.

I'm really hoping we can get around that entirely though; we have thus far.

Really, "all" we need to do is keep following the same pattern you've set forth, e.g. preserve everything we need to know about the last state in the VNode tree - in addition, we "just" need to provide the means for a functional component to access the lastNode and nextNode (it's own VNode) enabling it to inject/retrieve state and compare the last/next VNode between one render and the next.

Essentially, the signature of a functional component "just" needs to change from (props) => VNode to e.g. VNode => VNode, and the result needs to be spliced into the list of children of the parent of the functional component before we proceed to DOM diffing. This should make it possible for functional components to return either a single VNode, a list, or even null.

I think, if the functional component isn't represented as a VNode, there's nowhere to store any state, because that's the pattern we've committed to: diffing is based on the last/next state, we just need some means of allowing functions to inject state into their own VNode instance.

Alternatively, just thought of this... keep the current function signature like (props) => VNode, but with props having a reserved $state property or something - e.g. allowing the functional component to inject an object property, and the library only needs to make sure that this reserved property gets preserved in the VNode state and carries over between updates.

Would that work??

mindplay-dk commented 6 years ago

...sorry, that last bit is nonsense - it doesn't provide any means of actually generating a state change that triggers an update.

Come to think of it, I may have wasted my entire day pursuing these ideas, because none of this actually provides any means of triggering re-renders at component boundaries at all.

Fuck it, I give up :-(

jorgebucaran commented 6 years ago

@mindplay-dk Think of it as a relay race. You didn't waste your day, we're taking turns and now I have the baton. But I'm having a bad day so I'm running extra slow. Today's match is in an hour too, so there's that hehe. ⚽️

mindplay-dk commented 6 years ago

Yeah, sorry about that little outburst, I had a long week with too many dead ends and not enough results ;-)

mindplay-dk commented 6 years ago

@jorgebucaran looking at this again, I don't see any way to plug in any sort of component system at this point. What seems to get in the way is patchElement - operating on a single node at a time isn't going to work, because, as discussed, we need to defer the execution of (functional or otherwise) components to make that possible, which means we're effectively operating on two versions of the tree: one before running components, and one after.

The version of the tree before running components would govern component life-cycle, while the version after running components would govern the element life-cycle.

I've attempted to expand the entire tree before passing it to patchElement, but that's not going to work, since basically that means duplicating the entire reconciliation algo minus DOM manipulation.

I think what would work is refactoring patchElement to patchElements plural - have it operating on a range of elements rather than a single element. (Incidentally, this would also break the 1:1 parent/child limitation - you'd be able to render a range of elements to a single container.)

If patchElements operated on a range of elements (e.g. a range of child-nodes) it would now be able to alter that range of elements as it goes along.

So, whenever it would encounter a component, it would then first invoke that component and get the resulting rendered VNodes back - then simply splice those into the current list of children and continue processing from there. Again, this would also break the "must return a single child" limitation currently in functional components - they could return 0, 1 or many VNodes, we can splice any number of VNodes into the range we're currently processing.

This solves the case where a component returns 0 nodes, too - the component itself would still exist as a VNode (with a function in name) regardless of whether it expands to 0, 1 or many VNodes when rendered - the component VNode itself would get diffed against the previous component VNode first, and then after rendering and splicing-in it's result, diffing whatever it returned would continue from there.

Expanding in-place and splicing-in the rendered result is effectively what happens now in h - it just happens too soon and in the wrong order because evaluation order of h calls is inside-out.

This would also address a performance issue where a parent component looks at props.children and decides to nix the children for some reason - like, say, with something that shows/hides dynamically, like an expand/collapse panel or tabs, etc... functional components currently evaluate eagerly, which means they've already run by the time their rendered children get passed to their parent - which means the parent has no control over whether child VNodes get instanciated, it only controls whether they get displayed. So if you have a TabList component and some Tab components inside it, all your Tab components are going to render, regardless of which one is currently active. This would fix that.

Thoughts?

jorgebucaran commented 6 years ago

@mindplay-dk I don't understand what's the discussion about? I am not going to implement stateful components, this issue is closed.

mindplay-dk commented 6 years ago

I don't understand what's the discussion about?

Evaluation order.

This affects functional components.

For example, here's (enough of) an implementation of tab-panels in Preact:

https://codesandbox.io/s/jv1p71k5j5

Notice the console output:

rendering tab: one

The second occurrence of the functional component <Tab> never gets evaluated - not that this affects performance (since the JSX child-nodes get evaluated anyhow) but it enables <TabList> to access the actual <Tab> VNodes, and not just their output.

Porting this example to Superfine is of course possible:

https://codesandbox.io/s/04o2mq8n60

But notice the console output:

rendering tab: one
rendering tab: two

Again, this may affect performance, but that's not my main point.

The actual JSX node is inaccessible to TabList - or in other words, the Tab model is inaccessible to TabList.

I had to introduce accidental artifacts in the output of Tab, in the form of data-attributes, as a means of accessing information that was available in the Tab model.

And that work-around is only possible in the first place because the values happen to be strings - imagine if they were functions, numbers, booleans, and so on.

Moreover, that work-around is only possible because Tab always returns exactly one VNode - imagine use-cases where a functional component returns null or an array of VNodes.

My main point is, JSX is declarative - it's a model, in which intrinsic elements represent DOM elements, and functional components represent whatever they represent in the application's domain.

Eager evaluation effectively makes functional components a hidden implementation detail, which means you can't directly leverage functional components as a means of representing whatever it is they represent.

Eager evaluation essentially misses the model aspect of JSX.

jorgebucaran commented 6 years ago

@mindplay-dk As you know, Superfine/Hyperapp resolves components inside-out (unlike Preact/React). We've covered this before in Hyperapp when lazy components were introduced (as a matter of fact HAV1 works like this now, but HAV2 will not).

I know lazy components can be useful, but I want to avoid them (if I can) to keep things simpler. And yes, we don't need to write code like you suggested using data- attributes.

I rewrote the tabs demo using a state-first approach. Both Superfine and Hyperapp are meant to work like this (and not in any other way, e.g., querying VNode props).

CodeSandBox

import { patch, h } from "superfine"

const withLog = obj => (console.log("Rendering " + obj), obj)

const Tab = ({ id, label }) => (
  <div key={withLog(`Rendering tab: ${id}`)} class="tab-content">
    {label}
  </div>
)

const TabHeader = ({ tabs, select }) => (
  <div class="tab-headers">
    {tabs.map(tab => (
      <button class="tab" onclick={() => select(tab.id)}>
        {tab.label}
      </button>
    ))}
  </div>
)

const TabPanel = ({ tabs, activeId }) => (
  <div class="tab-panels">
    {tabs.filter(tab => activeId === tab.id).map(Tab)}
  </div>
)

const view = state => (
  <div class="tab-list">
    <TabHeader
      tabs={state.tabs}
      select={id => render({ ...state, activeId: id })}
    />
    <TabPanel tabs={state.tabs} activeId={state.activeId} />
  </div>
)

const app = (view, container, node) => state => {
  node = patch(node, view(state), container)
}

const render = app(view, document.body)

render(
  {
    activeId: "one",
    tabs: [
      { id: "one", label: "First Tab" },
      { id: "two", label: "Second Tab" }
    ]
  },
  document.body
)

I didn't understand the rest of what you said or disagree with it. Sounds like you wrote a lot of words, but didn't really say anything substantial. I'm glad you shared an example, so I could reply with one back.

mindplay-dk commented 6 years ago

I rewrote the tabs demo using a state-first approach. Both Superfine and Hyperapp are meant to work like this (and not in any other way, e.g., querying VNode props).

Hmm.

Well.

As implemented right now, we don't really have support for "functional components" - not in the sense that other frameworks may be capable of "treating functions as components".

Something like <App oncreate={...} ondestroy={...}/> simply doesn't work at all, because App doesn't have any life-cycle - it doesn't get created/updated/destroyed like other JSX elements.

Writing <App/> is fully equivalent to writing {App()}, so it's really just syntactic sugar for calling functions.

And maybe that's by design, but it seems inconsistent - client code can encode behavior dependent on the life-cycle of DOM elements (which is my favorite and uniquely powerful feature of this library) but can't encode any sort of behavior dependent on components.

I know you're not interested in any kind of stateful component, and my ideas/views are certainly evolving on this point - but the state-first approach doesn't provide any practical means of encapsulating accidental state. It doesn't provide any real modularity.

Imagine, for example, if every time you wanted an <input type="text">, you had to manually encode all sort of accidental state in your own model, such as focus/blur state, cursor position, selection, etc. - it would first of all be totally unmanageable, even at a small scale, but also, all of this state is transient and you expect it to disappear when the control disappears. If you close a form, you don't expect it to open at a later time with the cursor/selection in place, a date-picker still open, etc.

There is application state, and the state-first approach is great for encoding that - but there is also control state, and the state-first approach is terrible for encoding that.

Control state really pertains only to a control while it exists, and becomes completely irrelevant the moment the control disappears from the user's view: whether a date-picker or drop-down was open, where the cursor was located in an input, and so on.

We don't need or expect or want such state to persist in the model - most of the time, we don't even care that it exists, for example, we don't typically care if a drop-down is open, we just want a notification when a selection is made.

I would love to find better ways to encode control state than exists today - but the state-first approach seems to imply that all state is application-state, which isn't realistic or practical. Many DOM controls have accidental state, and, naturally, many custom controls will, too.

Application-state-first, yes, wonderful!

But state-first doesn't make sense for UI controls, which have certain states only while they exist.

To encode accidental control state in a component, it needs a life-cycle.

I don't know that there's any practical alternative. If there is, I'd love to learn about it, but it seems inevitable - accidental state has to go somewhere? :-)

jorgebucaran commented 6 years ago

@mindplay-dk I'm sure we discussed component lifecycle events in Hyperapp once (or twice) before. I think many people were of the opinion we don't need them, period. Maybe @SkaterDad wants to chime in a bit? I'm open to discussing it again, but I'm happy with fn(props+children) at the moment.

Writing is fully equivalent to writing {App()}, so it's really just syntactic sugar for calling functions.

Absolutely! JSX is just an "illusion". I like the XML syntax so I use it, but I am not married to it.

And maybe that's by design, but it seems inconsistent - client code can encode behavior dependent on the life-cycle of DOM elements (which is my favorite and uniquely powerful feature of this library) but can't encode any sort of behavior dependent on components.

It is by design, but I hear you too. Hey, I'd remove lifecycle events from elements as well (like I hope to do in Hyperapp eventually) and use Custom Elements for "naturally stateful things" (what you refer to as control state later in your comment), but I'm going to keep them in Superfine anyway. I want to break the rules (sort of) from time to time!

... but the state-first approach doesn't provide any practical means of encapsulating accidental state. It doesn't provide any real modularity.

We're going in circles. This is the same point about control state you are about to bring up later in your comment. Take elm apps for example. Their components are pure functions (just like our components). What they call "components" is not what React calls "components". They are just functions. It works.

Now, I am certainly not an expert in the field and I am still learning A LOT, but this is the path I want to follow. Please re/read Scaling the Elm Architecture. Also check out their RealWorld app subbmision (which reminds me I should build one for Superfine/Hyperapp).


There is application state, and the state-first approach is great for encoding that - but there is also control state, and the state-first approach is terrible for encoding that.

...for example, we don't typically care if a drop-down is open, we just want a notification when a selection is made.

I don't know that there's any practical alternative. If there is, I'd love to learn about it...

Custom Elements 💯

acstll commented 6 years ago

JSX is just an "illusion"

haha ❤️

mindplay-dk commented 6 years ago

I'd remove lifecycle events from elements as well (like I hope to do in Hyperapp eventually) and use Custom Elements for "naturally stateful things"

This was my hope as well - if you recall, I already wrote a thin UltraElement extends HTMLElement superclass, and initially was very excited about the prospect of not having to deal with components at all.

Sadly, DOM elements (and thereby custom elements) have an unfortunate property that makes them somewhat of a poor alternative to P/React components: you can update their individual state-properties one at a time only e.g. using either getters/setters or setAttribute.

This means you loose a critical aspect of functional (or stateful) components, which is atomic state updates - the functional component (or the render method of a stateful component) receives the entire set of coherent props as a single, atomic message.

Custom elements therefore must have a strategy for dealing with incoherent state - the intermediary states between updates applied to each of the element's properties/attributes could very well be invalid. Every implementation has to deal with this added complexity and come up with strategies for dealing with invalid states: you can't throw exceptions or clear invalid state (because being temporarily in an invalid state is "normal" for DOM elements) so you're essentially forced to deal with every imaginable invalid state.

For example, how do you render a slider control that is currently min = 0, max = 10, value = 100?

That state violates the domain constraints, but you have to render something - no matter what you render, it isn't going to make sense.

For this reason alone, I can't see custom elements replacing components in frameworks.

Please re/read Scaling the Elm Architecture. Also check out their RealWorld app subbmision

Interesting, never heard of this :-)

I looked at the Elm implementation, and, I mean, I don't know Elm, so maybe this makes sense to someone who can read the code, but man it's verbose.

I dug through the other implementations, and my hands-down favorite is the AppRun implementation - spent two minutes reading this and I already understand it.

AppRun also claims to follow "the elm architecture and event publication and subscription", but is somehow waaay more down-to-earth than this functional sciency gobbledigook ;-)

Seems to have some really nice ideas, I might even try it out - though I don't think this is going to bump Hyperapp 2 from the top of my to-do list :-)

By the way, thank you for carrying on these endless discussions - it's taking up way too much of my time, and I'm sure I'm taking up way too much of yours, but I really appreciate it. I hope you get something out of this too, but I suppose you'd have muted me by now if this was entirely too obnoxious ;-D

mindplay-dk commented 6 years ago

By the way, this RealWorld app doesn't really seem to have any custom controls with accidental state, so I'm not sure it's that helpful in the context of what I'm talking about.

Interesting real-world cases of accidental state I've run into so far include: a drop-down date-picker (collapsed/expanded state, month navigation state), any sort of property-sheet (with input states that don't apply to the model until you press "save") and a photoshop-style cropper. (keep the entire cropper state internally while dragging the corners and doesn't apply that state until you stop dragging.)

It seems like most people don't run into these cases, but for some reason I run into them all the time ;-)

SkaterDad commented 6 years ago

@jorgebucaran If what @mindplay-dk says about custom elements is accurate, I would still prefer to keep element-level lifecycle hooks as an escape hatch.

On vacation at the moment, so I can't comment much more on this or Hyperapp v2 until I'm home again.

mindplay-dk commented 6 years ago

@SkaterDad you could of course add your own setState() method to your own framework-dependent HTMLElement super-class, but other than that, DOM elements do not support anything other than individual updates via Javascript object-properties or setAttribute() - and technically you'd end up with some weird sort-of custom elements that really are more your own component-type that happens to leverage custom elements as a feature; your custom elements wouldn't really be custom elements, e.g. wouldn't work outside your framework, but you could go that route of course.

jorgebucaran commented 6 years ago

@mindplay-dk ...somewhat of a poor alternative to P/React components: you can update their individual state-properties one at a time only e.g. using either getters/setters or setAttribute.

@mindplay-dk This means you lose a critical aspect of functional (or stateful) components, which is atomic state updates – the functional component (or the render method of a stateful component) receives the entire set of coherent props as a single, atomic message.

@SkaterDad If what mindplay-dk says about custom elements is accurate...

False! 🎉

If it was true, then I'd be the first one to admit custom elements suck. But they don't. Custom elements rock. It's entirely up to you how to create custom elements. Yes, you could use setAttribute, appendChild, etc. But You are not going to. Period. You don't necessarily have to use any DOM APIs, at least not directly. You can build your custom elements using the same frontend stack you use for your apps. You can use React, Preact, Hyperapp, Superfine, Snabbdom, etc.

jorgebucaran commented 6 years ago

@mindplay-dk This was my hope as well... ...and initially was very excited about the prospect of not having to deal with components at all.

These comments came before you revelead the real flaw of your entire argument: that custom elements are not adequate. Let me reiterate that custom elements are perfectly adequate. You can create them using Superfine or preact or Hyperapp (and not using any DOM APIs directly). It's up to you! You choose your own adventure. To inifity and beyond! ⚡️


Having said that, no one is forcing you to use lifecycle events at all! See: https://github.com/hyperapp/hyperapp/issues/717.

jorgebucaran commented 6 years ago

@SkaterDad @mindplay-dk

https://github.com/jorgebucaran/superfine/issues/135#issuecomment-404581961

DOM elements do not support anything other than individual updates via Javascript object-properties or setAttribute() - and technically you'd end up with some weird sort-of custom elements that really are more your own component-type that happens to leverage custom elements as a feature; your custom elements wouldn't really be custom elements, e.g. wouldn't work outside your framework, but you could go that route of course.

Nope!

Use Superfine or Hyperapp instead. Or snabbdom, or go with Preact. 💥

jorgebucaran commented 6 years ago

tl;dr

SkaterDad commented 6 years ago

Sounds good to me. I'm looking forward to experimenting with custom elements soon!

mindplay-dk commented 6 years ago

I'm afraid you're completely missing my point.

Even if all of these frameworks update element attributes as a set, any underlying custom element being rendered by any of these frameworks is going to receive those updates individually, either via setters or via setAttribute() - there's no setAttributes() and even something like Object.assign() is just another way to call setters individually.

Custom elements, regardless of how they're implemented, have to deal with incoherent state.

jorgebucaran commented 6 years ago

@mindplay-dk I think you don't understand how "patching works" (in relation to VDOM) and I mean absolutely no disrespect. By all means, correct me if I am wrong, but remember this issue https://github.com/jorgebucaran/superfine/issues/39? 🙏

Have you understood by now that fastdom offers nothing to Superfine? At the time I wasn't really sure what that library was doing, and it was not until later that it became clear to me that the VDOM model offers the same benefit with regards to DOM thrashing.

jorgebucaran commented 6 years ago

any underlying custom element being rendered by any of these frameworks is going to receive those updates individually, either via setters or via setAttribute()

Custom elements being rendered using Hyperapp/Superfine will have its attributes updated as a set, just like your main app will be.

mindplay-dk commented 6 years ago

Custom elements being rendered using Hyperapp/Superfine will have its attributes updated as a set, just like your main app will be.

Via a custom non-DOM API, yes?

As I said:

technically you'd end up with some weird sort-of custom elements that really are more your own component-type that happens to leverage custom elements as a feature; your custom elements wouldn't really be custom elements, e.g. wouldn't work outside your framework

If OTOH you're going to update the properties/attributes of custom elements individually, as you do with regular DOM elements now, those custom elements must account for incoherent states, no matter how they were implemented.

Custom elements being rendered using Hyperapp/Superfine will have its attributes updated as a set

DOM properties/attributes do not get updated as a set - internally you're updating individual properties and attributes individually. Regular DOM elements internally do account for incoherent states.

jorgebucaran commented 6 years ago

@mindplay-dk I’m having a really hard time to understand all that you are saying. DOM props and attributes definitely get updated as a set. Why do you say the don’t?

And what do you mean by incoherent states?

acstll commented 6 years ago

To be somehow practical, though I do appreciate the discussion, I think it'd be helpful to add this (or something along these lines):

Components are evaluated immediately and you can't store state within them either. (https://github.com/jorgebucaran/superfine/issues/135#issuecomment-404698109)

to the Functional components bit of the README.

When I first built an app with Superfine (picodom at that time), I spent some time hunting what I thought was a bug, until I realised those functions were only being called once. I basically was expecting another behaviour.

Let me know if it makes sense for you that I contribute a little PR with that addition @jorgebucaran

jorgebucaran commented 6 years ago

@acstll Do please!

mindplay-dk commented 6 years ago

How do I put this plainly.

Consider this:

object.setA(1);
object.setB(2);

After the first call to setA(), can object know whether or not another call to setB() is coming?

Of course not, so whether the value being applied to A right now will be coherent with a value that may be applied to B later, object needs to deal with a potentially incoherent state when setA() gets called.

Contrast with:

object.set({ a: 1, b: 1});

In this case, object can consider the complete state-change as a single transaction - if it's current B value is somehow incoherent with the new A value, not a problem, because it already knows what the new value B is, which makes this transaction coherent.

React components work like the latter - DOM elements, and custom elements, work like the former.

Dealing with complete, coherent state changes is much simpler than deal with incremental, partial state changes - in your implementation, you will have to accept partial changes, even if they violate the internal domain constraints of your model.

Consider again the min/max/value example from before:

// first update cycle:

el.max = 100;
el.min = 0;
el.value = 50;

// second update cycle:

el.max = 10; // value of 50 is invalid now!
el.min = 0;
el.value = 0; // value is valid again

Now consider the transactional version of the same API:

// first update cycle:

el.set({
  max: 100,
  min: 0,
  value: 50
});

// second update cycle:

el.set({
  max: 10,
  min: 0,
  value: 0
});

The latter is much simpler to implement, as you don't need to decide what to do with temporarily invalid values - if value is out of range, you can immediately throw an error, whereas DOM (custom) elements have to accept it and deal with it at a later time.

With functional or stateful components, the component gets a single call with a complete state change - updating individual state-properties isn't even possible.

With custom elements, complete state changes aren't possible, at all - there is no DOM API that permits a custom element to receive anything other than individual state-properties, either via setters or setAttribute().

I don't know how to make this any clearer, but that is a fundamental difference from how updates to functional/stateful components are performed in most popular frameworks.

mindplay-dk commented 6 years ago

To be clear, I'm not necessarily criticizing custom elements - the architecture they came up with is consistent with the DOM, and probably has to be.

But DOM is a "document object model", and the API was designed to modify documents - it was never intended to be a UI framework, and something like custom elements has to abide by the idea of custom elements as mutable document nodes.

So, if you're building custom elements, that's what you're building: document nodes with mutable state - and just like native DOM elements, you will have to deal with invalid/incoherent state.

It's not necessarily a disaster, but it is something quite drastically different from the functional/stateful components we're seeing in today's frameworks.

And as said, you don't strictly have to build custom elements that work outside of your own framework - for example, you could add a framework-specific setState() method as an API extension, and simply don't support any attributes or public properties. You're not exactly building standard custom elements then, just leveraging the feature for your own purposes - which might be okay.

jorgebucaran commented 6 years ago

While I appreciate you taking the time to write all this, I'd prefer to argue with code in the future. Here is something to get the discussion moving forward.

https://codepen.io/anon/pen/WKrexZ?editors=0010

import { h, patch } from "https://unpkg.com/superfine?module"

class CustomTitle extends HTMLElement {
  static get observedAttributes() { return ["count", "title"] }

  constructor() {
    super()
    this.root = this.attachShadow({ mode: "open" })
    this.state = {}
  }

  view(state) {
    return h("div", {}, [
      h("h1", {}, state.title),
      h("h1", {}, state.count)
    ])
  }

  update() {
    console.log("Patching...")
    this.node = patch(
      this.node, 
      this.view(this.state), 
      this.root
    )
  }

  schedule() {
    if (this.isScheduled) return
    this.isScheduled = true

    setTimeout(() => {
      this.isScheduled = false
      this.update()
    })
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(name, oldValue, newValue)
    if (oldValue === newValue) return
    this.state[name] = newValue
    this.schedule()
  }
}
customElements.define("custom-title", CustomTitle)

const view = count =>
  h("div", {}, [
    h("custom-title", { count, title: count % 2 ? "Hey!" : "Hi!"  }),
    h("button", { onclick: () => render(count - 1) }, "-"),
    h("button", { onclick: () => render(count + 1) }, "+"),
  ])

const app = (view, container, node) => state => {
  node = patch(node, view(state), container)
}

const render = app(view, document.body)

render(10)
jorgebucaran commented 6 years ago

With custom elements, complete state changes aren't possible, at all - there is no DOM API that permits a custom element to receive anything other than individual state-properties, either via setters or setAttribute().

In the example above I am batching state updates since Superfine doesn't do it out of the box, but Hyperapp does.