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

Web Component strategy discussion #19

Open
samselikoff opened this issue Jun 28, 2017 · 34 comments
Open

Web Component strategy discussion #19

samselikoff opened this issue Jun 28, 2017 · 34 comments

Comments

@samselikoff
Copy link

samselikoff commented Jun 28, 2017

Overview and motivation

Glimmer was created primarily as a tool for building mobile Progressive Web Applications (PWAs). This means the main focus of development has been on performance, namely reducing the library's file size and improving render speed.

However, Glimmer's small size and friendly programming model gave rise to another use case: using Glimmer components as Web Components (WC).

When Glimmer shipped as a standalone library at EmberConf 2017, it came with a simple exporter that could build top-level Glimmer components as WCs. Since WCs are extremely low-level, this made the integration story for using Glimmer with other JavaScript technologies like Ember and React — and even server-side technologies like Rails — very compelling: simply load a single JavaScript file and drop your WC into any HTML template.

Several members of the community work in organizations that use multiple technologies like Ember, React and Vue alongside each other. These orgs have found themselves in a place where it's difficult to share code across technologies. Glimmer's WC story provides one possible solution to this problem. Further, if these orgs use Ember, the case for using Glimmer to author sharable components is even stronger: eventually Glimmer Components will be able to be used directly inside of Ember applications and run on the same Glimmer VM that ships with Ember, thereby eliminating the need for a WC wrapper.

Glimmer's small size and emphasis on one-way data flow make it a great tool for authoring reusable WCs. There are some open questions about how we should design the WC interface.

Design decisions

The design decisions to be made are mostly concerned with how to get data into and out of a Glimmer-enabled WC.

We are proposing using string attributes and blocks to get data in, and custom events to get data out.

<acme-button color="green">
  Click me
</acme-button>
document.querySelector('acme-button').addEventListener('dismiss', function() {
  // You invoked the `dismiss` custom event on <acme-button>
});

Attributes

WCs have robust support for string-only attributes:

<acme-button color="green">

The color attribute is available via the getAttribute API, and there's also an attributeChangedCallback that gets invoked when the color attribute changes. These APIs allow us to make the attribute available to the Glimmer Component, both on initial render and on re-render.

HTML attributes are string-only, so the attributes approach begs the question: how do we get high-fidelity JavaScript data into our Glimmer-backed WCs?

<acme-table data=jsArray />

Something like the above is not valid HTML - there is no HTML-only syntax to support it. Of course, we are used to doing things like this in Ember and other frameworks:

{{acme-table data=jsArray}}

Behind the scenes, Ember/HTMLBars is going to set the jsArray as a JavaScript property on the actual DOM Node. Similarly, Glimmer supports passing data between components using args:

<acme-table @data={{jsArray}} />

But the important point here is that while all the frameworks have solutions for passing high-fidelity JavaScript data between components, there is no API in the WC spec to support this from just an HTML template.

Having the rendering part of the WC story rely solely on HTML is part of what makes it such a compelling solution: HTML is supported in all client-side frameworks out of the box (since it's the core building block of the web), and it's robust to server-side rendering (since servers already produce string-based HTML templates).

It's also worth noting that a high-fidelity JavaScript object could be stringified into JSON and passed into a Glimmer-backed WC as a string attribute, and the Glimmer component could then deserialize and use it. In this way a Glimmer-backed WC could accept JavaScript data — but it would lose any this context.

So, while JavaScript properties will undoubtedly be needed for certain components, they should not be at the core of the basic rendering strategy. Attributes are a robust API suitable for many use cases, despite their notable downside of being string-only.

Blocks

In addition to Attributes, the second way to pass data into a WC is with a block:

<acme-button>
  Hello, <strong>world!</strong>
</acme-button>

Passing in a block to a custom element seems like something everyone will (reasonably) expect to be able to do, and fortunately WCs have support for slots. So this API is possible.

Glimmer's {{yield}} API is of course similar to <slot> but it would not be used directly here. Instead, the top-level Glimmer Component would use the slot tag to render the passed-in block:

{{! my-glimmer-component/template.hbs }}
<button>
  <slot></slot>
</button>

Events

Now that we've described the two ways of passing data into a WC, the last point to discuss is how to get data out. We propose using addEventListener with CustomEvents:

// Glimmer component
onClick() {
  let event = new CustomEvent('dismiss', { some: 'data' });
  this.dispatchEvent(event);
}

// consumer
document.querySelector('acme-button').addEventListener('dismiss', function() {
  // You invoked the `dismiss` custom event on <acme-button>
});

Standard events like onclick will work on the WC's root element, but custom events give component authors more control over the behavior of their components, and addEventListener is a well-supported API.

Wrapper components in Ember/React/etc.

Given a Glimmer-backed WC built using the patterns above, an Ember Component wrapper might look something like the following:

// components/acme-button/component.js
export default Ember.Component.extend({

  tagName: 'acme-button',
  
  attributeBindings: [ 'color' ],
  
  didInsertElement() {
    this.element.addEventListener('dismiss', this.get('dismiss'));
  },

  willDestroyElement() {
    this.element.removeEventListener('dismiss', this.get('dismiss'));
  }
  
});	

This Ember component could then be used throughout the Ember app like this



{{#acme-button color=value dismiss=(action 'foo')}}
  Hello, <strong>world</strong>!
{{/acme-button}}

which feels like a first-class Ember component. Similar wrappers could be made in any other framework. There's also a possibility here for building tools to generate these framework-specific components from annotations in the Glimmer Component's source, since the only data that's really required to define the wrapper is the tag name, attributes list and events list.

{

  tagName: 'acme-button',

  attributes: [ 'color' ],

  events: [ 'dismiss' ]
  
}

Finally, if frameworks like Ember add template-based helpers to attach events via addEventListener, the wrapper component wouldn't be needed at all.

<acme-button color={{value}} {{add-event-listener 'dismiss' (action 'foo')}}>
  Hello, world
</acme-button>

More things to discuss

Need to explain why we need a wrapper component.

Not sure how Ember would handle a bare Glimmer-backed WC:

<acme-button color={{value}} dismiss=?>
  Hello, world
</acme-button>

but I believe the color attr might work, but there's no way to set the dismiss function in the template (dismiss={{action 'foo'}} doesn't work, that sets a dismiss property on DOM Node if property exists, otherwise sets fooFunction.toString as an attribute). I believe the slot would work.

Shadow DOM

@robdodson
Copy link

robdodson commented Jul 7, 2017

It's awesome that Glimmer is diving into Web Components! I've heard similar stories from large partners, that they want to share components amongst their teams but everyone is on different stacks so it can be challenging.

I think the approach you have here sounds good but I had a question regarding the attributes/properties proposal. If I defined a glimmer wc with an exposed color attribute, would it also create a corresponding color property under the hood? The reason I ask is because HTML attributes have some weird behaviors that make them good for initial configuration but possibly dangerous to rely on if you're trying to read back the value later. A classic example is checkbox:

<input id="checkbox" type="checkbox" checked>
<script>
  checkbox.checked = false;
  console.log(checkbox.hasAttribute('checked')) // logs true
  console.log(checkbox.checked) // logs false
</script>

I believe this behavior comes from the dirty value flag in the HTML spec.

As a result libraries like React think of attributes on native HTML elements as being useful for initial configuration, but beyond that they read and set values using properties. Sebastian speaks about it a bit in this clip.

If defining a Glimmer component attribute also creates a corresponding property under the hood, and the two are kept in sync, then I think it will make the component easier to use in different frameworks.

Today a library like Preact will check to see if the property is defined (I think using hasOwnProperty) and set it. If it's not defined then it will fallback to setting an attribute. Vue, on the other hand, will always attempt to set attributes on a custom element, but provides special syntax for explicitly setting properties <my-component :prop="someThing"></my-component>.

In my own vanilla web components I support setting the property or the attribute. In instances where the tag gets lazy loaded and a framework sets a property before the element has been upgraded I use this technique to grab the property from the instance and pass it to the element's setter.

edit:
A bit more explanation on the lazy properties thing: https://developers.google.com/web/fundamentals/architecture/howto-components/howto-checkbox#lazy-properties

@samselikoff
Copy link
Author

Hey Rob, thanks for chiming in! I was actually just reading through some of your Dev Channel articles and comments in React's issues. I'm still wrapping my head around all of the constraints so our overall strategy is still very flexible at this point.

It seems like treating attributes as initial configuration might be the right mental model. Last week we were toying with an API that would let Glimmer component authors specify whether a local property should map to an attribute or a property on the custom element, but now I'm thinking this might be the wrong direction.

So if we instead map all attrs to props and keep the underlying prop in sync when the attr changes, then Glimmer component authors would just need to specify what attributes their components expose; and that would only be for the purpose of initial configuration, limited to string-only attrs, and robust to server-side rendering. Component behavior that goes beyond that would work via instance properties (which can be set in different ways by the various frameworks, or by node.foo = {} in JS-land). Sound right?

@samselikoff
Copy link
Author

samselikoff commented Jul 9, 2017

Another development since my initial post has to do with events and functions.

My current use case is getting some nice reusable WCs to work in both Ember and React. As you pointed out in the React issues, it would be nice if React offered some sugar for calling addEventListener from templates. There was some talk about using on* properties as sugar for calling addEventListener(*) but some folks said this feels brittle and I feel more or less in agreement.

In Ember we attach actions via <x-component dismiss={{action 'foo'}} /> but Ember takes care of the event wiring, and none of these helpers desugar to addEventListener. There's been some talk about adding a new primitive in Ember that would let us write helpers to call JavaScript code in the context of the node, meaning we could end up with something like

<x-component {{add-event-listener 'dismiss' (action 'foo')}}>

but nothing like this exists today. This would mean to use a WC that emits custom events in an Ember app today, you'd need to create a wrapper component. It would be really nice to avoid this, so we were also toying with having our Glimmer components invoke function properties, instead of dispatching custom events.

This would mean you could drop a custom element into your element app with no wrapper, write

<x-component dismiss={{action 'foo'}}>

and be off to the races. (Internally, the Glimmer component would call this.dismiss(), so callers wouldn't ever use addEventListener - they just set the property as a function and it would be invoked at the appropriate time).

If and when Ember, React and other frameworks add nice sugar for calling addEventListener from template-land, we could start telling authors to dispatch custom events. But this might be a short-term solution that would make the integration story much nicer today.

Thoughts on this?

@robdodson
Copy link

So if we instead map all attrs to props and keep the underlying prop in sync when the attr changes, then Glimmer component authors would just need to specify what attributes their components expose; and that would only be for the purpose of initial configuration, limited to string-only attrs, and robust to server-side rendering. Component behavior that goes beyond that would work via instance properties (which can be set in different ways by the various frameworks, or by node.foo = {} in JS-land). Sound right?

Yep that sounds good to me!

(Internally, the Glimmer component would call this.dismiss(), so callers wouldn't ever use addEventListener - they just set the property as a function and it would be invoked at the appropriate time).

If I were to use the Glimmer custom element outside of Ember, like in a vanilla JS app, what would that look like?

querySelector('x-component').dismiss = someHandler ?

I guess my concern with that approach is around burning in patterns. Like if it ends up encouraging folks to not dispatch Custom Events so you end up with an ecosystem where Custom Elements all work a little differently. For what it's worth, Preact, Angular, and Vue all support listening to Custom Events. It means if I write a vanilla Web Component, I know it'll work in all those contexts. Even React supports it if you do the ref workaround.

@tomwayson
Copy link

@samselikoff should we expect the approach that you originally laid out above ("using string attributes and blocks to get data in, and custom events to get data out.") to work today in an Ember app (possibly with an Ember component as a wrapper if needed)?

@shivgarg5676
Copy link

May I know the advantages of using glimmer components over web components. Do they provide performance benefits over web components? Like stated in the strategy glimmer web components are easier to share across platforms but how? Because web components can be shared among platforms.
Organisations which feel it difficult to share code among platforms should write web components. What are the high-level features that we will be able to provide using glimmer's web components when sharing them among different platforms?

Any update on https://github.com/glimmerjs/glimmer-web-component/issues/20 ?

@samselikoff
Copy link
Author

samselikoff commented Jul 16, 2017

@shivgarg5676 After several weeks of experimentation we need to recollect our thoughts on our general approach. We've learned a lot about the constraints and requirements for actually sharing components across web frameworks in the real world, and it seems it's not quite as simple as "just export web components."

Several of us are working on this but at the moment, you should not expect things to just work without a significant amount of extra effort on your part.

@domenic
Copy link

domenic commented Jul 18, 2017

I'd caution that the properties/attributes issues @robdodson mentions are very specific to input (and to some extent the cancer has spread to other form controls). In general HTML is designed to have perfect attribute <-> property reflection, with some problematic legacy exceptions like input. I don't think it's good to draw a general conclusion that attributes are for initial state and properties are for current state. In general, they should be in sync, both reflecting the current state. You can see this in the design of most every other HTML element.

@rjcorwin
Copy link

@domenic Perhaps a shim to make form controls consistent?

@samselikoff
Copy link
Author

@domenic thanks for chiming in. What about non-string properties like Objects and Functions?

@domenic
Copy link

domenic commented Jul 18, 2017

It depends. For example, relList is an object reflected in the rel attribute. But indeed, some HTML elements have imperative APIs that are not configurable via attributes. That doesn't detract from the fact that all their data is generally reflected by both attributes and properties.

@samselikoff
Copy link
Author

I see. If instructing folks on best practices when authoring custom elements, what would the explanation be for syncing properties back to attributes when possible? Could you lay out the argument?

Also as you pointed out, there are certain APIs that don't reflect or aren't reasonable to reflect. Some examples that come to mind are div.onclick and the <video> element's play() and pause() methods. Can we come up with a rule or heuristic on which APIs ought to be reflected back to attributes, and why?

@domenic
Copy link

domenic commented Jul 18, 2017

what would the explanation be for syncing properties back to attributes when possible? Could you lay out the argument?

If they're trying to create custom HTML elements, then they should behave like HTML elements. Author expectations are that in general you can manipulate attributes to change data, as that's the case for almost every HTML element and its attributes in HTML. Everyone expects <google-map latitude="50" longitude="40"></google-map> to work; they don't expect you to need to use script to set those properties.

I invite you to Ctrl+F https://html.spec.whatwg.org for "reflect" to find all the instances where they are kept synchronized.

Can we come up with a rule or heuristic on which APIs ought to be reflected back to attributes, and why?

In general, attributes should be the primary way you put data in your custom element, or get it out. They are the source of truth; properties are just getters/setters that do return this.getAttribute(...)/this.setAttribute(...). At least, that's how it is in the HTML spec.

You can layer sugar APIs on top of it for performing imperative steps if you want; that's up to you as an element author. But your element should always be usable without having to invoke those sugar APIs.

div.onclick is reflected as the onclick="" attribute.

@samselikoff
Copy link
Author

samselikoff commented Jul 18, 2017

I just tried this and perhaps my understanding is off, but it looks like onclick is not reflected? At least not the same way title is

kapture 2017-07-18 at 17 38 27

@domenic
Copy link

domenic commented Jul 18, 2017

Try the other direction.

@robdodson
Copy link

robdodson commented Jul 18, 2017

Catching up a bit here.

In general, they should be in sync, both reflecting the current state.

Yes I agree and tried to make this point as well in my initial comment when I said:

If defining a Glimmer component attribute also creates a corresponding property under the hood, and the two are kept in sync, then I think it will make the component easier to use in different frameworks.

Regarding:

In general, attributes should be the primary way you put data in your custom element, or get it out. They are the source of truth; properties are just getters/setters that do return this.getAttribute(...)/this.setAttribute(...). At least, that's how it is in the HTML spec.

I've been trying to think about this from the framework author's perspective and what would make the most sense for them to implement. As you pointed out, input has some quirky behavior so they need to special case it. I'm not sure if there are other elements that have similar behavior? Also you can't really pass an object or array through an attribute, so in those situations they'd need to set a property. I think ideally the framework should have syntax which allows the developer to set the property or the attribute, but for those that don't (React, Preact), I've encouraged them to use properties because it's the most consistent.

For custom element authors, I've encouraged reflecting all primitive values back to attributes, and keeping them in sync so it doesn't really matter how someone is using your element.

@samselikoff
Copy link
Author

samselikoff commented Jul 18, 2017

Also you can't really pass an object or array through an attribute, so in those situations they'd need to set a property. I think ideally the framework should have syntax which allows the developer to set the property or the attribute

For me this is the heart of the issue I'm trying to understand most. React devs have come to expect to be able to render <MyChart data=series />, Ember devs {{my-chart data=series}} and so on, where series is a high-fidelity JavaScript object (array of objects, for example) not representable in pure HTML.

So the question is, given that (1) modern developers have come to expect to be able to pass complex JavaScript data into their components, and also (2) we want to provide a tool for folks to author "components for the web" and desire to those components to hew close to the spirit of HTML elements, usable in many environments present and future, what is the advice we should give developers for when they should use attributes and when they should use properties?

@wycats
Copy link

wycats commented Jul 19, 2017

Stepping back for a moment...

At a very basic level, the point of web components is to provide an extension point that the HTML parser understands. Web components hook into HTML Markup, which is a big part of what makes them different from imperative solutions that came before.

Before web components, if I wanted to distribute a component that required users to call functions, assign properties, and update values, I had plenty of ways to do that, and the resulting component would be plenty "interoperable". Just write a function that takes a "mount point" and set up some setters on the object you return. The thing I couldn't do was write a component that you could stick into markup and have the HTML parser do something with.

And this isn't just a pedantic, semantic difference. A lot of people understand HTML. A lot of people are maintaining websites who know how to use markup, but who struggle with the more imperative styles of programming. The great thing about web components is that they give people who understand basic HTML a way to natively use a component without having to understand even basic JavaScript semantics. This is profoundly empowering.

Here's an example of <google-maps> from webcomponents.org:

<google-map fit-to-marker api-key="AIzaSyD3E1D9b-Z7ekrT3tbhl_dy8DCXuIuDDRc">
  <google-map-marker latitude="37.78" longitude="-122.4" draggable="true"></google-map-marker>
</google-map>

Anyone who knows HTML knows how to use this. And that's how it should be!

Now, you might also want to build a framework for the web, and you might want to use web components as a substrate for that framework. Like any other framework, you're gonna need ways to pass rich data to the elements. And that's fine! Like Ember, Angular and React, you'll invent some custom syntax for passing data from one part of your framework to another.

But that custom syntax is not HTML and not a part of any web standard.

Poke around https://www.webcomponents.org if you don't believe me:

<paper-toolbar class="medium-tall">
  <paper-icon-button slot="top" icon="menu"></paper-icon-button>
</paper-toolbar>

The best web components are written to be used with HTML syntax, where attributes mean attributes, and where properties are set (by advanced users) in JavaScript.

At a fundamental level, HTML is HTML and attribute syntax is attribute syntax. Because interoperable web components have a basic need to be used in HTML markup, they simply have no choice but to provide interfaces that work with attributes.

In my opinion:

  • when invoking a web component from HTML, attribute syntax means attributes
  • if you want to modify a web component's properties or addEventListener, no problem! Do that in JavaScript.
  • if a web component is highly reliant on "setting properties from HTML" for plausible usage, it is, in my view, a poorly behaved web component, because it will be unusable by normal HTML authors.

TLDR: We shouldn't confuse how web components built for Polymer model data flow with how well-behaved web components are meant to be used from markup.

@wycats
Copy link

wycats commented Jul 19, 2017

A couple of follow-ups:

In order to clearly distinguish between attributes and "data passed through the framework", Glimmer uses attribute syntax to mean attributes and @argument={{value}} to mean "Glimmer data flow." If a Glimmer component is meant to be used as a web component, it should offer a attributes-based API so that it can be used by authors who want to use pure markup.

It also makes sense for Glimmer to provide wrappers that make it easy to use our components from other web frameworks like Angular, React and Polymer. But because those frameworks need a way to pass pure data directly into Glimmer, the wrappers should use the syntax that is native to the embedding framework.

For example, to use a Glimmer component in React, it would make sense to use JSX and <GlimmerComponent prop={value} /> syntax, and have that map onto Glimmer "args".

To use a Glimmer component from Polymer, it would make sense for the wrapper to use Polymer-local style and invoke as <glimmer-component prop="{{value}}">.

The point is: a universal "web component" wrapper should use attributes, while a wrapper for a specific web framework should use whatever semantics the embedding environment would use to pass pure data.

@robdodson
Copy link

robdodson commented Jul 19, 2017

when invoking a web component from HTML, attribute syntax means attributes

Sorry if I wasn't clear earlier, I think we're all in agreement on that. I would certainly never encourage someone to make a properties-only web component. I think Domenic's point, which I agree with, is there should be an attributes interface that maps to properties and the two stay in sync.

if you want to modify a web component's properties or addEventListener, no problem! Do that in JavaScript.

I agree with that too. I think some frameworks provide abstractions to make this a little easier for devs (Preact for example will look at anything that starts with on* and setup addEventListener under the hood for you, so you can do <x-foo onClick={handleClick}>. But that decision is up to the framework authors to add as syntactic sugar. A web component shouldn't rely on anything like that. It should just dispatch events.

if a web component is highly reliant on "setting properties from HTML" for plausible usage, it is, in my view, a poorly behaved web component, because it will be unusable by normal HTML authors.

yep I agree with this as well.

TLDR: We shouldn't confuse how web components built for Polymer model data flow with how well-behaved web components are meant to be used from markup.

I was a little confused by this as I don't think Polymer prescribes a specific style. If you define a property on a Polymer element it automatically exposes an attribute as well and the two are kept in sync so folks should be able to use either.

In order to clearly distinguish between attributes and "data passed through the framework", Glimmer uses attribute syntax to mean attributes and @argument={{value}} to mean "Glimmer data flow."

So if I were trying to pass an object to a custom element I would use the @argument syntax, correct?

@wycats
Copy link

wycats commented Jul 19, 2017

I was a little confused by this as I don't think Polymer prescribes a specific style. If you define a property on a Polymer element it automatically exposes an attribute as well and the two are kept in sync so folks should be able to use either.

Polymer provides a syntax for passing rich data to a component:

<some-component some-prop="{{richData}}"></some-component>

This (imo confusingly) looks like an attribute but is actually the way the Polymer framework passes rich data to its components by setting properties. It's not using HTML semantics, and that syntax would not work without the Polymer framework as a host.

I'm saying we shouldn't confuse this HTML-superset feature of Polymer with the design of markup meant to be used with interoperable web components.

So if I were trying to pass an object to a custom element I would use the @argument syntax, correct?

No. Just as you wouldn't use some-prop="{{value}}" in Glimmer (or React or Vue) for passing data to a Polymer web component (because it's Polymer-specific syntax), and wouldn't use some-prop="{{value}}" for passing data to a React component from Polymer (because Polymer doesn't understand the React component model), you wouldn't (at present) use @arg={{value}} for passing data from Glimmer to Polymer.

It's just custom, HTML-superset syntax for communicating with Glimmer components.

We've considered creating an "element modifier" (in Glimmer parlance) for declaratively defining properties on a custom element:

<some-component {{prop richData=value}}></some-component>

The idea behind "modifiers" (helpers that run inside of elements rather than attributes or text content) is that they serve as "lightweight components". They get a lifecycle like a regular component, but can be attached to any element and used to encapsulate imperative semantics.

@robdodson
Copy link

<some-component {{prop richData=value}}></some-component>

I see, I think Vue has something similar as part of v-bind where you can add a .prop modifier. So if I knew I really wanted to set a property on a Custom Element instead of setting an attribute I could do:

<some-component :richData.prop=value></some-component>

Preact on the other hand will do a 'richData' in element check and if it exists it'll set the property, otherwise it will set the attribute. Angular has syntax so you can specify if you want the attribute or the property to be set. And React always sets the attribute.

If the Custom Element is written well and can handle corresponding attributes and properties then all of the above work fine. The one exception is when you try to pass rich data in React and you end up with <some-component richData="[object Object]"></some-component> which isn't very useful.

@wycats
Copy link

wycats commented Jul 19, 2017

If the Custom Element is written well and can handle corresponding attributes and properties then all of the above work fine.

I agree. My main point is that a well-written web component simply must implement attributes as the "interface from markup", because otherwise usage from normal HTML will fail. All else being equal, then, attributes are the maximally compatible way of interacting with web components.

The Glimmer approach also has the benefit of guaranteeing that HTML copied from webcomponents.org will "just work", even if the user starts using dynamic data, without having to teach the user to switch into "rich data" mode.

The one exception is when you try to pass rich data in React and you end up with which isn't very useful.

React has a double-problem because most of their API is "props-first", so it's easy to transfer those habits into web component land incorrectly. I'd argue that one obvious-and-correct way to pass rich text in React is to use the lifecycle hooks and set properties imperatively.

@wycats
Copy link

wycats commented Jul 19, 2017

Preact on the other hand will do a 'richData' in element check and if it exists it'll set the property

This feels pretty broken to me in a web component world because it's not particularly unidiomatic in JS to lazily add properties.

class MyComponent extends HTMLElement {
  constructor() {
    super();
    this.addEventListener('click', () => this.color = "red");
  }

  set color(value) {
    this.style.backgroundColor = value;
  }
}

Obviously this isn't a well-written web component, but the point is that an in check may not always be sufficient to understand the "intent" of a web component. On the other hand, since web components need to work in pure-HTML environments, if a web component is documented to use attributes, attributes will work.

@robdodson
Copy link

I'm not sure I understand this example. The in check would evaluate to true and Preact would call the setter. Is that not what you want in this scenario?

Also the element should have a corresponding color attribute as well. I think to Domenic's earlier point, that setter for color should just call setAttribute('color', value) and in the attributeChangedCallback any side effects would be handled (in this case setting the background color).

@wycats
Copy link

wycats commented Jul 19, 2017

@robdodson in this case, the in check would evaluate to false, because the color property is added lazily.

My point isn't that this is a well-written web component. It's not. My point is that it's easy for the in check to fail, while leaning on the attribute-based API that people need anyway (to make HTML work) is less likely to hit these kind of gotchas.

UPDATE: Ah I see. I used a setter, which would cause the in check to pass. It wasn't the point of my example. You can imagine various ways that people could rely on lazy property setting in poorly written web components. I can try to find a better example if that would be helpful.

@wycats
Copy link

wycats commented Jul 19, 2017

I think to Domenic's earlier point, that setter for color should just call setAttribute('color', value) and in the attributeChangedCallback any side effects would be handled (in this case setting the background color).

I think this is, indeed, the right way to implement web components, and illustrates that even web components that are trying to expose a property-based API will respond correctly to attributes as a reasonable, interoperable, lowest-common-denominator.

@robdodson
Copy link

oh I see, I did a test and it came back true so I was confused. Ok, yes I see what you mean.

My point isn't that this is a well-written web component. It's not. My point is that it's easy for the in check to fail, while leaning on the attribute-based API that people need anyway (to make HTML work) is less likely to hit these kind of gotchas.

Yeah, just didn't want to miss the opportunity to try to clarify what we mean by a well written component 😊

I think this is, indeed, the right way to implement web components, and illustrates that even web components that are trying to expose a property-based API will respond correctly to attributes as a reasonable, interoperable, lowest-common-denominator.

Agreed.

I definitely feel like I understand your perspective now (and thanks for spending the time to share it with me!)

I think my only final question is back to the point on objects and arrays. Today the only way to set an object property is imperatively, e.g. myElement.richData = {foo: 'bar'}. In the future Glimmer may support element modifiers like <some-component {{prop richData=value}}></some-component> so I can do it declaratively. But at present, is there any way to do this declaratively in Glimmer or is the recommendation to manually set the property in JavaScript?

@legaev-stas
Copy link

legaev-stas commented Aug 2, 2017

Colleagues, as for me the right way of passing rich data into Web Component is passing nested xml structure. Some of native HTML elements parse nested xml structure and render accordingly to it. Also some of them have strict list of possible nested html elements.
select is a good example:

<select>
  <option value="1">One</option>
  <option value="2" selected>Two</option>
  <option value="3">Three</option>
</select>

On example of Glimmer.js that will be:
<custom-select @collectinon={{ [{text: "One", value: 1, selected: false}, {...}, {...}] }}>

Another example is table:

<table>
  <colgroup>
    <col span="2">
    <col width="50%">
  </colgroup>
...
</table>

If we open a debugger and select in tree option, colgroup or col, we won't find highlighted element on the screen. I think, because parental element parsed them, get rich data and rendered itself accordingly. Also if we invoke selectbox.selectedIndex it will provide the state of selectbox but in reality selected element is option.

Is there any point in my words?

@caseywatts
Copy link

caseywatts commented Jan 5, 2018

I'm really interested in using glimmer for a web component that [accepts an attribute as an ~argument], and I link people to this issue a bunch 🙂

In the meantime, I got something sorta working using didUpdate and a local variable _hasInitializedAttributes (it won't get any updates, but that's okay in my use-case):
https://github.com/caseywatts/arlingtonruby-glimmer/blob/08b6a005742d7a27591df66c7d49b5a6d2fa2251/src/ui/components/meetup-card-list/component.ts
I've got a feeling this isn't performant - I think it's called forever in a loop lol - but it helped me play with the shape of what it might look like.

@santo74
Copy link

santo74 commented Nov 1, 2019

Now that Ember Octane is right around the corner, what's the status of all those web-component related issues?
More specifically, is it already possible to pass in attributes to a Glimmer web component from a static html page?
e.g.: <my-glimmer-web-component my-attr="customval"></my-glimmer-web-component>

Also, is it possible to use slots?

@santo74
Copy link

santo74 commented Nov 15, 2019

I know this is an old issue, but nevertheless it's still relevant so it would be nice to get an update on this (please also see my post above).

My use case is as follows:
I'm using Ember.js for my app but there are more and more situations where I have to use some functionalities from regular html pages (e.g. forms). Because I couldn't reuse our Ember components for this I'm currently using Vue.js for this (via vue-custom-element), but it means I have to write those components twice (Ember for our app, Vue for use with html pages).
So obviously it would be much nicer if I could use Ember for this as well.
Therefore I tried to create a Glimmer component and use it as a custom element.
This is working fine, however I can't find if/how I can pass any attributes to it.

So any updates are greatly appreciated, especially regarding the use of attributes and slots.
@wycats do you by any chance have some updates on this?

@argarcia-ottersoft
Copy link

any update on this?

@JenLyndle
Copy link

We have an Ember app and currently using Atomico to create web components and import them and use it inside the Ember app. Our development velocity will be much higher if we could create glimmer components which can be exported as web components when needed. Also having the shadow DOM will be super cool as then we'll not have to rely on CSS specificity to prevent style leakage. Very interested to see if there is an update to this dev effort.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests