-
Notifications
You must be signed in to change notification settings - Fork 11
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
Comments
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 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: |
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 |
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 In Ember we attach actions via <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 If and when Ember, React and other frameworks add nice sugar for calling Thoughts on this? |
Yep that sounds good to me!
If I were to use the Glimmer custom element outside of Ember, like in a vanilla JS app, what would that look like?
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 |
@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)? |
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. Any update on https://github.com/glimmerjs/glimmer-web-component/issues/20 ? |
@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. |
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. |
@domenic Perhaps a shim to make form controls consistent? |
@domenic thanks for chiming in. What about non-string properties like Objects and Functions? |
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. |
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 |
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 I invite you to Ctrl+F https://html.spec.whatwg.org for "reflect" to find all the instances where they are kept synchronized.
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 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.
|
Try the other direction. |
Catching up a bit here.
Yes I agree and tried to make this point as well in my initial comment when I said:
Regarding:
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, 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. |
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 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? |
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-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:
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. |
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 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 To use a Glimmer component from Polymer, it would make sense for the wrapper to use Polymer-local style and invoke as 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. |
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.
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
yep I agree with this as well.
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.
So if I were trying to pass an object to a custom element I would use the |
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.
No. Just as you wouldn't use 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. |
I see, I think Vue has something similar as part of
Preact on the other hand will do a 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 |
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.
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. |
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 |
I'm not sure I understand this example. The Also the element should have a corresponding |
@robdodson in this case, the My point isn't that this is a well-written web component. It's not. My point is that it's easy for the UPDATE: Ah I see. I used a setter, which would cause the |
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. |
oh I see, I did a test and it came back true so I was confused. Ok, yes I see what you mean.
Yeah, just didn't want to miss the opportunity to try to clarify what we mean by a well written component 😊
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. |
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.
On example of Glimmer.js that will be: Another example is 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? |
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 |
Now that Ember Octane is right around the corner, what's the status of all those web-component related issues? Also, is it possible to use slots? |
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: So any updates are greatly appreciated, especially regarding the use of attributes and slots. |
any update on this? |
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. |
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.
Attributes
WCs have robust support for string-only attributes:
The
color
attribute is available via thegetAttribute
API, and there's also anattributeChangedCallback
that gets invoked when thecolor
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?
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:
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: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:
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 theslot
tag to render the passed-in block: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: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, andaddEventListener
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:
This Ember component could then be used throughout the Ember app like this
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.
Finally, if frameworks like Ember add template-based helpers to attach events via
addEventListener
, the wrapper component wouldn't be needed at all.More things to discuss
Need to explain why we need a wrapper component.
Not sure how Ember would handle a bare Glimmer-backed WC:
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 adismiss
property on DOM Node if property exists, otherwise sets fooFunction.toString as an attribute). I believe the slot would work.Shadow DOM
The text was updated successfully, but these errors were encountered: