Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal to improve the DOM creation api #150

Open
straker opened this issue Jan 14, 2016 · 36 comments
Open

Proposal to improve the DOM creation api #150

straker opened this issue Jan 14, 2016 · 36 comments
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest

Comments

@straker
Copy link

straker commented Jan 14, 2016

I'm sorry if this topic has been discussed already, I tried to do due diligence but couldn't find a similar proposal on the forums or the html or dom github repos.

The DOM creation api is a bit cumbersome to work with. To create a single element with several attributes requires several lines of code that repeat the same thing. The DOM selection api has received needed features that allow developers to do most DOM manipulation without needing a library. However, the DOM creation api still leaves something to be desired which sways developers from using it.

There are several use cases where the api is cumbersome to use. I have compiled a gist of just a few of them. It shows several use cases where the current api requires an awkward solution, and demonstrates a few common hacks of working around the native api to get a more manageable result. It also provides several examples of how popular libraries handle the same use case, usually in a simpler manner.

I would like to propose that the DOM creation api be improved so that developers have a cleaner interface into DOM creation and no longer need libraries to do it.

@annevk
Copy link
Member

annevk commented Jan 14, 2016

There has been discussion here: https://lists.w3.org/Archives/Public/www-dom/2011OctDec/thread.html#msg20. There was also http://www.hixie.ch/specs/e4h/strawman though TC39 didn't really like it. There is https://github.com/domenic/element-constructors by @domenic which should be finished at some point.

I don't think libraries have really converged on something here either, other than innerHTML-like approaches (which would suggest the E4H approach, perhaps amended to use template strings).

Also note that the libraries that use object-like notation often confuse content attributes and IDL attributes (aka JavaScript properties), which makes this rather tricky.

@straker
Copy link
Author

straker commented Jan 14, 2016

Thanks for those resources, they were very enlightening.

From those discussions, it seems that template strings (or quasi-literal templates from the public archives) seem to be where the discussions keep coming back to, and seem to have generated the most consensus. With ES2015 template strings appearing to fulfill the role of the quasi-literal template proposal, I'm guessing the discussion should be focused on how to use the template string on the parent node?

Domenic's element constructors sounds a bit like Dart's solution to create a new constructor for elements (Dart went a bit farther and made one for each element). It looks like he uses the object-like notation to define namespaces or attribute declarations, so would it suffer from the same problem of confusing content attributes and IDL attributes?

@annevk
Copy link
Member

annevk commented Jan 14, 2016

Template strings seem like a way forward, though making them work with the HTML parser seems tricky. I think that is why nobody has tried thus far. Element constructors might indeed have that same problem, though they are also solving a more fundamental problem, that these classes don't have constructors. Which I think is why we want them either way, even if they didn't have convenient syntax for attributes and such.

@straker
Copy link
Author

straker commented Jan 14, 2016

Ok. I'll go play with how browsers interpret different template strings and the HTML parser and see where it doesn't behave as expected. I'll post my findings here and focus on HTML parsing of template strings. Once a consensus on how to handle HTML parsing of template strings has been reached, the discussion could then turn into how to implement a convenient syntax for using the template string on the parent node.

Do you know of any more resources to discussions on template strings and the HTML parser?

@straker
Copy link
Author

straker commented Jan 19, 2016

It seems that the discussion around E4H and template strings is more complicated than I would like this proposal to address. In the end, both template strings and E4H rely on the already existent DOM apis of appendChild or innerHTML to work with the DOM and don't introduce any new ones. https://lists.w3.org/Archives/Public/www-dom/2011OctDec/thread.html#msg20 eventually wound up going towards template strings, but it looked like they were proposing a new browser implemented function called html that would convert a string using the HTML parser (which reverts back to the E4H and template string debate).

With that in mind, I would like to propose adding new DOM apis that make these use cases much cleaner and easier to carry out:

  1. creating a single node with attributes
    • if this could also handle the use case of creating a single node and it's children in a single step, that'd be great
  2. creating a series of sibling nodes to be appended to a parent

These apis could then be used by either E4H or template strings, which ever one wins out in the end. If this new api is the html function, it shouldn't go through the HTML parser if it'll have problems such as https://lists.w3.org/Archives/Public/www-dom/2011OctDec/0170.html was describing. Instead, it should just accept the string it's given and create a tr with it's children, regardless if the node doesn't have context.

(a very simple approach that mimics the behavior would be)

function html(str) {
  var match = str.match(/<([^>]*)>/);
  var rootStr = match[1].trim().split(/\s/);
  var tagName = rootStr[0];
  var root = document.createElement(tagName);

  // attributes
  for (var i = 1; i < rootStr.length; i++) {
    var attr = rootStr[i];
    var name = attr.substring(0, attr.indexOf('='));
    var value = attr.match(/=['"]?([^'"]*)/)[1];

    root.setAttribute(name, value);
  }

  // children
  root.innerHTML = str.replace(match[0], '');  // kinda hakcy as it will ignore the orphaned closing tag

  return root;
}

console.log(html(`<tr class="foo" data-config=bar>
  <td class="hello">
    <span class="foo">bar</span>
  </td>
</tr>`));
console.log(html(`<tr>`));

@straker
Copy link
Author

straker commented Feb 9, 2016

So I stumbled upon tagged template strings, which makes it seem that template strings and E4H can live in harmony. It would seem that you could remove (deprecate, etc.) the innerHTML function that takes a string and instead strictly use an HTML tagged function. The function can now understand the insertion points of the template string and can do E4H style processing to return safe DOM. This tagged function could also be used to just generate dom by itself.

This would be the new API to creating DOM that would satisfy my uses cases.

var text = "foo";

// `html` is the tagged template function that runs E4H

// create a single node with attributes
document.body.appendChild( html`<div class="bar">${text}</div>` );

// create a series of sibling nodes
var element = html`<div>${text}</div><div>bar</div>`;

@annevk
Copy link
Member

annevk commented Feb 9, 2016

Yeah, I think that kind of API would be ideal. @freddyb, any sanitizing library should look like the above ^^. Making this work with the HTML parser is a lot of work unfortunately.

@straker
Copy link
Author

straker commented Feb 9, 2016

I thought one of the benefits of E4H was that you didn't have to go through the HTML parser. Either way, I would be happy to help get something like this working. What can I do to help?

@annevk
Copy link
Member

annevk commented Feb 9, 2016

That is true, E4H had a much simpler grammar. I don't know if folks would find that acceptable though. They probably expect similar parsing rules to <template> so you can write <img> as <img> and not <img/>, omit </td>, etc.

I don't know how familiar you are with the HTML parser, but figuring out what adjustments would need to be made to make html...`` work would be a good first step. Coupled perhaps with a basic algorithm for that template string function.

@domenic
Copy link
Member

domenic commented Feb 9, 2016

Personally I think the best first step is to produce a library with the desired semantics and have it get reasonable adoption, before we consider standardizing it. It's still early days for template strings and to me it doesn't make sense to talk about standardizing a template string tag yet.

@annevk
Copy link
Member

annevk commented Feb 10, 2016

Yeah, maybe. At some point we need to make sanitizing easier and add it to browsers. That will require a similar API of sorts. And also, this is hugely complicated to get right. @straker buliding a prototype on top of https://github.com/inikulin/parse5 (or similar library) might be a good first step here.

@straker
Copy link
Author

straker commented Feb 10, 2016

Sounds good, I'll get to work getting a prototype using a tagged template function built on top of a parsing library. I'll also see if I can incorporate some of the E4H ideals into it just as a proof of concept.

@domenic
Copy link
Member

domenic commented Feb 10, 2016

I'm not sure why you need a parsing library exactly? Can't you just use

document.createElement("template");
template.innerHTML = passedInStringAfterSubstitutions;
return template.contents;

Maybe the problem is safely generated passedInStringAfterSubstitutions. but maybe it's not; just do HTML escaping and then concatenation. See e.g. https://github.com/domenic/count-to-6/blob/master/lib/exercises/tagged_template_strings/solution/solution.js

@straker
Copy link
Author

straker commented Feb 10, 2016

If you didn't care that you always escaped the substituted DOM that would work, but what happens when I trust the substitution (e.g. I created it) and don't want to escape it so it will create DOM instead of string escaped text?

I'm guessing that this contributes to what makes this problem hugely complicated to get right.

@domenic
Copy link
Member

domenic commented Feb 10, 2016

I guess that's where contextual auto escaping comes in.

@annevk
Copy link
Member

annevk commented Feb 10, 2016

@domenic you want to be able to set an attribute value without having to account for whether or not the passed in value included " or ' or whitespace (in case it's unquoted). Similar for the contents of an element and such.

@straker
Copy link
Author

straker commented Feb 20, 2016

Alright, here is the initial draft of the html tagged template https://github.com/straker/html-tagged-template.

I combined the best principles of E4H and contextual auto escaping to prevent XSS attacks, and it turned out pretty well if I do say so myself. What I would love now is help from security experts, like Mike Samuel who wrote about contextual auto escaping, to further the XSS prevention since I don't have a lot of experience in that area.

Also, I'm not sure the best way to allow HTML variable substitution to be marked as safe so it isn't escaped when added to the DOM.

@domenic
Copy link
Member

domenic commented Feb 20, 2016

This does look pretty cool. I hope people use it.

My one big problem is that I don't like the overloaded return type (sometimes a node, sometimes an array). I think I would probably prefer a DocumentFragment all the time. Or maybe two helpers, one that throws if more than one element is parsed, and one that always gives an array? or always gives a document fragment?

@straker
Copy link
Author

straker commented Mar 2, 2016

So I'm not sure how to proceed from here. There's been some nice discussions on the repo, but I feel that unless something changes, it won't go much further than where it currently is. Do you have any suggestions?

@annevk
Copy link
Member

annevk commented Mar 2, 2016

I think the main hindrance is it becoming a popular way to create elements, maybe even the defacto way. And probably if we were to add an API like that I'd like the default to be safer than innerHTML. E.g., have XSS filtering by default and require the usage of unsafeHTML...`` to do what the library does now.

(Then there's also other activity around the HTML parser that likely takes priority for implementers, such as making custom elements work and providing a streaming API around the HTML parser (once we have sorted out network streaming).)

@caub
Copy link

caub commented Aug 11, 2016

I find it pretty bad to make string templates, as soon as it's on client-side, (and you more likely need to keep references, add/remove event listeners, ...

I'd really like that document.createElement become like React.createElement

and be able to do: (with const h=document.createElement below)

h('ul', {className:'something'},
  items.map( ({text})=> h('li', {onClick: e=>{/*..*/}}, 
      text,
      h('span', {onClick: close}, '✕')
    )
  )
)

@domenic
Copy link
Member

domenic commented Aug 11, 2016

That seems way worse than

h`<ul class="something">${items.map(({text}) => h`
  <li onclick=${e=>/*..*/}>
    text
    <span onclick=${close}>✕</span>
  </li>`}
</ul>`

@caub
Copy link

caub commented Aug 11, 2016

Ah thanks, indeed, https://github.com/straker/html-tagged-template (is this the right link?) looks interesting, similar to jsx. I'm just sceptic on how events are added

After seeing https://github.com/straker/html-tagged-template/blob/master/index.js I don't see anything for event listeners, and all thoses regex feel quite dirty (ofc jsx parser might be similar).. My approach seems unnatural at first-see, but pretty practical, if not more after a bit of training

@straker
Copy link
Author

straker commented Aug 11, 2016

The proposal only dealt with the creation of DOM. Adding event listeners can still be done through the normal ways once the DOM is created.

var dom = html`<div>
  <button>Click me</button>
</div>`

dom.querySelector('button').addEventListener('click', function() { /* ... */ });

@caub
Copy link

caub commented Aug 12, 2016

I saw your implementation, here's a short one that does Domenic's example https://gist.github.com/caub/da489a286b0098d0fcd799b66a252196#file-h-js

@annevk annevk added the needs implementer interest Moving the issue forward requires implementers to express interest label Mar 16, 2017
@annevk annevk marked this as a duplicate of #477 Jul 18, 2017
@justinfagnani
Copy link

justinfagnani commented Jul 26, 2017

I've been working on a similar library recently and was pointed to this issue as a place that might be interested.

The library is called lit-html, and it uses template literals, but not to create Nodes immediately, but to create templates that can be efficiently updated with new data later.

https://github.com/PolymerLabs/lit-html

The syntax is quite similar, though the templates are usually going to be in a function:

const template(data) => html`<ul>${data.map(d=>html`<li>${d}</li>`}</ul>`;

The big difference is that the result of the tag is a TemplateResult containing a Template and the expression values. This can then be rendered multiple times to the same container:

render(data) {
  const result = html`<ul>${data.map(d=>html`<li>${d}</li>`}</ul>`;
  result.renderTo(document.body);
}

You can call render() multiple times and only the dynamic parts/expressions will be updated.

The updating strategy and API is based on discussion on standardizing <template> expressions had here: whatwg/html#2254 When a template is cloned/instantiated, it creates both nodes and Part objects, which can be updated independently of the rest of the template instance.

lit-html is really a layering of two parts which could be looked at for platform support:

  1. Parsing special JS template literals as <template> with placeholders for expressions.
  2. Something like Standardize <template> variables and event handlers html#2254 to allow efficient updates of <template> clones.

I've tried to make the design extensible so that additional opinionated features can be layered on top. There's an included extension that allows templates to set properties on elements by default instead of attributes, and supports declarative event handlers.

const button = (data) => html`
  <my-button
      class$="${data.isPrimary ? 'primary' : 'secondary'}"
      on-click=${_=>data.onClick}
      someProperty=${data.state}>
    ${data.label}
  </my-button>
`;

(you can see that here: https://github.com/PolymerLabs/lit-html/blob/master/src/labs/lit-extended.ts )

I also prototyped stateful template helpers, with an implementation of a repeat() function that renders a list of keyed data and will reuse and reorder the DOM nodes from previous renders:

const list = (items) => html`
  <ul>
    ${repeat(items, (i) => i.uniqueId, (i, index) => html`
      <li>${index}. ${i.title}</li>
    `}
  </ul>
`;

(repeat is here: https://github.com/PolymerLabs/lit-html/blob/master/src/labs/repeat.ts )

Definitely looking for feedback. As far as standardization, I know we'd need to get this to be popular first, which I think is possible...

/cc @rniwa

@jonathantneal
Copy link

Template Literals are awesome, but I have a non-template string proposal at #477 which may align more closely with how this discussion started.

@caub
Copy link

caub commented Jul 26, 2017

it's this more or less?

/*
// html helper, usage
var span = $('span', 'Hello')
var div = h('div', {id:'test', onClick:console.log}, span, 'test2')
*/

const safeAttrs = new Set(['textContent', 'id', 'className', 'htmlFor', 'disabled', 'checked', 'autocomplete', 'crossorigin', 'async', 'innerHTML']); // probably missing some

function $(tag, ...o){
	const el = document.createElement(tag);
	const childrenIndex = o.findIndex(x => typeof x=='string' || typeof x=='number' || x instanceof Node);
	const props = Object.assign({}, ...o.slice(0,childrenIndex);
	for (var k in props) {
		const value = props[k];
		if (typeof value=='function') {
			const name = k.slice(2).toLowerCase();
			el.addEventListener(name, value);
		} else {
			if (k=='style') Object.assign(el.style, value);
			else if (safeAttrs.has(k)) el[k] = value;
			else if (typeof value=='string') el.setAttribute(k, value);
		}
	}
	el.append(...o.slice(childrenIndex+1));
	return el;
}

@jonathantneal
Copy link

@caub, in a few ways it’s similar, like the node name as the first argument and recognizing functions as events. The business with safe attributes to cleverly assign CSS seems like the stuff of DOM libraries. I don’t want clever, and I separated my proposal into parts just in case I had anything similarly over-reaching.

Everyone’s got a gimmick now

I just want to easily create elements. Elements typically host attributes, events, and other nodes. That’s it. Doing this natively right now is a behemoth.

This proposal doesn’t even solve for namespaced tags or namespaced attributes. And CSS can use selectors.

.append() is a fantastic example of a well constructed API. The motivation is simple — I want to append nodes easily. Nodes are typically elements or text.

I think the similarities in libraries does represent a cowpath. I just also think libraries suffer because they are always trying to be so clever.

@rianby64
Copy link

I can't imagine how to handle the order of appearance of attributes in a Custom Element.
If for some reason you'll make possible to create an element with some attributes then give me a chance to put the attributes as an array or something similar.

I may assume that the order of appearance could be achieved at observedAttributes.

class TestHTMLElement extends HTMLElement {
  constructor() { super(); }
  attributeChangedCallback(name, oldValue, newValue) {
    // I want to see here the order or reaction
    // as indicated in the observedAttributes
  }
  static get observedAttributes() {
    return ['attr1', 'attr4', 'attr2', 'attr3']
  }
}

I expose this idea because when you use createElement, then you're free to setup the attributes in the order you need.

@caub
Copy link

caub commented Aug 17, 2017

@straker I've done it this way: https://github.com/caub/dom-tagged-template

@straker
Copy link
Author

straker commented Aug 23, 2017

I think this issue is getting a bit out of hand and scope. It started off as just wanting to create a simpler interface to DOM creation. We added XSS prevention as a bonus to developers to prevent many serious problems in the web. But now it's been suggested to handle adding event listeners, having binding support and delayed rendering, and keeping attribute order.

I feel, as Alex Russell did in his Polymer Summit talk today, that we are trying to solve all problems with a single tool. As such, I wonder if we should be trying to solve each of these problems as their own APIs instead of solving everything with a single API.

For example, @justinfagnani suggestion for a binding aware template string could be built on top of a simpler interface to DOM creation, so long as the interface allowed for that to happen. That way we can build up multiple small things that all support one another, just like the Chrome team did to create 20+ different suggestions to different APIs to support Web Components.

image

@caub
Copy link

caub commented Aug 23, 2017

@straker Did you look at my code? yes event listeners are definitely important, I thought your version did it, and XSS is not an issue in my case

I could make it work with SVG, shadow DOM too, good idea

@fregante
Copy link

fregante commented Sep 19, 2017

A simple, consistent API would be good.

An API compatible with JSX transformed code would be grand:

  1. Write JSX

    document.body.append(<div>Hello world!</div>);
  2. Set pragma: Element.create

    /** @jsx Element.create */
  3. Run babel. Done.

    document.body.append(Element.create(
      "div",
      null,
      "Hello world!"
    ));

Live demo on babel repl


To be clear I'm asking to make the h package into a standard, not the JSX syntax:

h (tag, attrs, [text?, Elements?,...])

Its code is quite short, 63 lines: https://github.com/dominictarr/h/blob/master/index.js

@fregante
Copy link

fregante commented Sep 19, 2017

That said, @straker's template literal suggestion is actually quite good-looking, with the exception that it's still string-based and thus slightly error prone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest
Development

No branches or pull requests

8 participants