minui / docs

Documentation and general communication space
1 stars 0 forks source link

Problem: returning both an API and an element #2

Closed ronkorving closed 8 years ago

ronkorving commented 8 years ago

Since we won't depend on virtual DOM, we have the challenge that however the user invokes an instantiation, the user needs easy access to both the HTML element (so they can append it to their document) as well as the instance's API.

Feedback welcome.

Proposal 1: array return values

The instantiation always returns an array [api, Element]. This can be a convention that users can get used to hopefully easily, but also importantly, if users can use destructuring they can quickly assign both items to a separate variable.

With destructuring:

var [list, elm] = dropdown();
list.add('Hello');
list.add('World');
document.body.appendChild(elm);

Cons:

var list = dropdown();
list[0].add('Hello');
list[0].add('World');
document.body.appendChild(list[1]);

Cons:

Components always expose the element on their instance using a fixed name (eg: elm, nice and short).

var list = dropdown();
list.add('Hello');
list.add('World');
document.body.appendChild(list.elm);

Cons:

The object that exposes the API is also a function that you can run and will always return the element.

var list = dropdown();
list.add('Hello');
list.add('World');
document.body.appendChild(list());

Cons:

ronkorving commented 8 years ago

An example implementation for proposal 3:

export default function dropdown() {
    // create the element

    var elm = document.createElement('div');
    elm.className = 'minui-dropdown';

    // element return function

    function out() {
        return elm;
    };

    // API on top of the element return function

    out.add = function (child) {
        if (typeof child === 'string') {
            var text = child;
            child = document.createElement('div');
            child.textContent = text;
        }

        elm.appendChild(child);
        return this;
    };

    out.remove = function (child) {
        elm.removeChild(child);
        return this;
    };

    return out;
};
ronkorving commented 8 years ago

An example implementation for proposal 2:

function Dropdown() {
    this.elm = document.createElement('div');
    this.elm.className = 'minui-dropdown';
}

Dropdown.prototype.add(child) {
    if (typeof child === 'string') {
        const newChild = document.createElement('div');
        newChild.textContent = child;
        child = newChild;
    }

    this.elm.appendChild(child);
};

Dropdown.prototype.remove(child) {
    this.elm.removeChild(child);
};

export default function dropdown() {
    return new Dropdown();
};
qubyte commented 8 years ago

My 2:dollar:...

1) Appeals to me because it keeps the element and the API separate. What bothers me about it is the semantics of the thing. What I mean by that, is that clearly one field (or index) is the element, and can be named as such. The other field you've named API, and while I can't think of anything better it seems somehow fluffy. The object or array that contains these two things has no clear name to me, and that really bothers me. No obvious name tends to indicate a poor or improper separation of concerns, though I might be mistaken. If this is always used with destructuring, perhaps it's fine not to have a name for it at all. Destructuring also appears to be counter to composition here.

2) Is the backbone way, so you might find many people familiar with that approach. It benefits from the entire thing being a presenter-object, with the element as part of its API, which is going to be drop-in-able for many existing projects. The issue that bothers me with this is that the presenter object acts as a sort of gatekeeper for the element, and often gets used to proxy events. There are lots of pitfalls there with retaining references to elements (though those can be mitigated with WeakMap now). On the other hand, I've been experimenting a lot with backbone-like things lately and I've always avoided a presenter (View in backbone parlance) implementation, which might be telling.

3) This is interesting, and I wouldn't have considered this approach. I'm not sure there is a clear advantage to having methods attached to the function though. If list is called twice, would it produce the same result, or would the first invocation clear the state? If the latter, it might be nicer to feed the invocation a configuration object. The former seems odd, and could also probably be reduced to something else.

4) Initially I misread 3 to be a function returning an element with some additional behaviour. This feels like the way things will be in the future with custom elements, but we're not quite ready for that yet. I'm not sure how I feel about mixing the bloated API of an element with custom attributes and methods. If we choose this route, we can apply mixins as a stopgap.

I see no clear winner, but if I had to pick one it'd probably be 4, a function optionally taking some configuration and returning an element with some additional methods:

const dropdownMethods = {
  add(child) {
    this.appendChild(child); // This is kinda artificial here, I know.
  },
  remove(child) {
    this.removeChild(child);
  }
};

export default function dropdown() {
  const element = document.createElement('div');
  element.className = 'minui-dropdown';

  return Object.assign(element, dropdownMethods);
}

Not perfect, but less objects floating around! I really rambled on there...

ronkorving commented 8 years ago

We had a conversation on Skype, and Brian really sold me on using custom elements for this project. There's at least one very capable and compatible polyfill, and Chrome currently supports it natively. It should be up to the user to pick a polyfill (if any) imho. I hope other browsers will offer native support soon. I've made a few demos (offline) that really work quite well.

There are some massive benefits that no other solution can bring to the table:

To me, these positives are just way too legion to ignore. Polymer gets it right (except they don't, because they built a system around it that I for one don't want).

For more backlog, please read the Skype chat.

Caniuse.com: http://caniuse.com/#feat=custom-elements Polyfill for document.registerElement: https://github.com/WebReflection/document-register-element

qubyte commented 8 years ago

I'll catch up on that chat in a bit, but here's the problem... The custom element API just changed, and it doesn't look like it did before (that's the link I sent the other day). Currently no browser implements it (caniuse is out of date).

It'll look like:

class CustomButton extends HTMLButtonElement {}

customElements.define('custom-button', CustomButton, { extends: 'button' });

The last line parameter can be omitted in some cases.

ronkorving commented 8 years ago

So here's the good news: that's all inward facing. Implementing the change would have absolutely no impact on the user experience. Ideally, the (or any) polyfill would simply be updated and we can move to the new API soon. But again, I don't see this impact the UX.

ronkorving commented 8 years ago

Oh by the way, I wrote a tabbar system based on custom elements to see this work (and it works): https://gist.github.com/ronkorving/3dbc17aea7534ccdb48a

qubyte commented 8 years ago

True! It can be abstracted if necessary. Alrighty then, we're all on board!

ronkorving commented 8 years ago

Schweet!

qubyte commented 8 years ago

Re-reading, it doesn't look like that old API is going away anyway, so cool :+1:

ronkorving commented 8 years ago

:+1: Cool If we're all on the same page then on using custom elements, I'm gonna close this issue. Thanks guys.

ronkorving commented 8 years ago

Btw, this is how Apple is moving: https://bugs.webkit.org/show_bug.cgi?id=150225