-
Notifications
You must be signed in to change notification settings - Fork 25.7k
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
feat(elements): implement @angular/elements
#19469
Conversation
1e27a28
to
3e825a1
Compare
You can preview 3e825a1 at https://pr19469-3e825a1.ngbuilds.io/. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Few comments. @robwormald will have more feedback for you.
Also I think we decided to cal it just @angular/elements
so you will need to do search and replace.
// (i.e. not transpiled to an ES5 constructor function). | ||
// TODO(gkalpak): Document that if you are using ES5 sources you need to include a polyfill | ||
// (e.g. https://github.com/webcomponents/webcomponentsjs/blob/master/custom-elements-es5-adapter.js). | ||
class NgElementConstructorImpl extends NgElementBaseImpl<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have FooImpl
? Why not just Foo
. There is but one implementation.
See: https://github.com/angular/angular/blob/master/docs/NAMING.md
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what you mean. There are interfaces called NgElementConstructor
and NgElementBase
, so I suffixed the implementations with Impl
(note that all Impl
-suffixed things are not public API).
Open to suggestions for a different naming scheme 😃
* TODO(gkalpak): Add docs. | ||
* @experimental | ||
*/ | ||
export interface NgElementBase<T> extends HTMLElement { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this interface adds value. Just go directly to implementation skip interface and remove -Impl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation has public members (properties, methods, constructor) that I don't want to document.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, I was thinking about renaming these as follows:
// Before:
NgElement = NgElementBase & {[prop in keyof P]: P[prop]}
NgElementBase { /* the actual interface without custom props */ }
// After:
NgElementWithProps = NgElement & {[prop in keyof P]: P[prop]}
NgElement { /* the actual interface without custom props */ }
Would it be clearer/less clattered?
disconnected, | ||
} | ||
|
||
interface NgElementBaseImplConnected<T> extends NgElementBaseImpl<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interface by definition can't be Impl
import {NgElementBaseImpl, NgElementInput, NgElementOutput} from '../src/ng-element'; | ||
import {setScheduler} from '../src/utils'; | ||
|
||
export function main() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
main
used to be for Dart. It is no longer needed, you can skip it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm...not so sure about that 😉
3e825a1
to
27efe36
Compare
You can preview 27efe36 at https://pr19469-27efe36.ngbuilds.io/. |
@@ -0,0 +1,22 @@ | |||
{ | |||
"name": "@angular/custom-elements", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
think we're going with @angular/elements
here
|
||
// NOTE: This is a (slightly improved) version of what is used in ngUpgrade's | ||
// `DowngradeComponentAdapter`. | ||
// TODO(gkalpak): Investigate if it makes sense to share the code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want to support this case at all? it seems like it should be unsupported, since we're not going to be able to get any of the @ContentChild/Children stuff to work here?
<slot>
handles this natively, what's the benefit of doing this over relying on platform behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess ContentChild(ren)
don't work with projected content. What else doesn't work?
I don't feel strongly about supporting it. In my head, the idea is to have something work as similar as possible when inside and outside an Angular app (so I can re-use components are "native" Angular components or custom elements).
readonly selector: string; | ||
readonly observedAttributes: string[]; | ||
|
||
upgrade(host: HTMLElement): NgElement<T, P>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this your method name? Worried about a potential conflict with a future native upgrade
thing which has been discussed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. I thought it makes sense to call it something that describes what it is actually doing (in CustomElements terms). In the design docs it is called attach()
(which is more arbitrary).
I wasn't aware of any discussions about a native upgrade
thing. Are there any resources you can point me to?
export function createNgElementConstructor<T, P>(appRef: ApplicationRef, injector: Injector, componentFactory: ComponentFactory<T>): NgElementConstructorInternal<T, P> { | ||
const inputs = componentFactory.inputs.map(({propName, templateName}) => ({ | ||
propName, | ||
attrName: camelToKebabCase(templateName), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is kinda sketchy - seems like we should avoid these heuristics where possible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem this is trying to address is that most of the time attribute names are the same as property names (unless explicitly overriden), which in turn are usually camelCased. CamelCased attributes are fine when something is compiled with Angular's compiler, but we can't have them in native HTML.
So, we either need to lowercase them or kebabcase them. (@mhevery said kebabcasing is fine.)
// (e.g. https://github.com/webcomponents/webcomponentsjs/blob/master/custom-elements-es5-adapter.js). | ||
class NgElementConstructorImpl extends NgElementBaseImpl<T> { | ||
static readonly selector = componentFactory.selector.toLowerCase(); | ||
static readonly observedAttributes = inputs.map(input => input.attrName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems to conflate attributes and @inputs ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I understand the question, but probably yes 😁
According to the design, updating an attribute will propagate to the corresponding property (not the other way around though) and attributes will be considered for initializing properties (when we instantiate the component).
// TODO(gkalpak): Document that if you are using ES5 sources you need to include a polyfill | ||
// (e.g. https://github.com/webcomponents/webcomponentsjs/blob/master/custom-elements-es5-adapter.js). | ||
class NgElementConstructorImpl extends NgElementBaseImpl<T> { | ||
static readonly selector = componentFactory.selector.toLowerCase(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be static is = componentFactory.selector.toLowerCase();
- matches with Polymer nicely?
Why are we lowercasing the selector? We should not allow the following usages:
- foo-Bar - (case sensitive)
- foobar - violates the CE spec
- [foo-bar] - doesn't make sense in CE land
(tldr, we should require people to always use dash-case
selectors, because the spec requires it :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to rename selector
to is
(although I think selector
is more clear 😁).
The lowercasing is just to be consistent with the casing, so that we can use it to match against nodeName
s later without having to lower/uppercase it every time.
I have also thought about "validating" the selector and throwing if it is not a dash-case
selector. In practice, I don't think it is possible to instantiate an element that hasn't been added to the customElements
registry, which means that customElements.define(selector, ...)
will throw anyway.
But maybe it will be possible to bypass the customElements
registry in the future, so 👍 for having a explicit check for the selector.
static readonly selector = componentFactory.selector.toLowerCase(); | ||
static readonly observedAttributes = inputs.map(input => input.attrName); | ||
static readonly onConnected = new EventEmitter<NgElement<T, P>>(); | ||
static readonly onDisconnected = new EventEmitter<NgElement<T, P>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what are these for? can we not just use the lifecycle hooks themselves?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These emit values when an NgElement of this type is connected/disconnected.
How would we use the lifecycle hooks for that?
443e5d7
to
ea73465
Compare
custom-elements
@angular/elements
945fff2
to
663e924
Compare
a9a81a1
to
6a7a490
Compare
You can preview 6a7a490 at https://pr19469-6a7a490.ngbuilds.io/. |
karma-js.conf.js
Outdated
@@ -34,6 +34,9 @@ module.exports = function(config) { | |||
'node_modules/zone.js/dist/async-test.js', | |||
'node_modules/zone.js/dist/fake-async-test.js', | |||
|
|||
// Include polyfills necessary for testing the `custom-elements` package. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't it be elements
package now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch 😃
6189089
to
caaa98e
Compare
You can preview ead31ec at https://pr19469-ead31ec.ngbuilds.io/. |
ead31ec
to
21bfaf2
Compare
You can preview 21bfaf2 at https://pr19469-21bfaf2.ngbuilds.io/. |
@gkalpak is it expected to have a feat on a patch branch (5.0.x) ? |
Hm...probably not. I had it marked as Thx for catching this 👍 |
Thanks for you PR! |
🎉 |
This PR was merged without API docs and general rollout plan. We can't release this as is in 5.1 without a plan for documentation, cli integration, etc.
This PR was merged without API docs and general rollout plan. We can't release this as is in 5.1 without a plan for documentation, cli integration, etc.
This PR was merged without API docs and general rollout plan. We can't release this as is in 5.1 without a plan for documentation, cli integration, etc.
This PR was merged without API docs and general rollout plan. We can't release this as is in 5.1 without a plan for documentation, cli integration, etc.
Was this merged in elsewhere ? @gkalpak |
Should this be reopened or is there some other way to see progress on this feature? (I checked the lab branch and it doesn't look like anything has changed since the backed out merge.) |
it is a live feature ? it will be in the future ? :) last commit in: https://github.com/angular/angular/tree/labs/elements oct.12 :( |
Not sure what you mean by "live feature". It is not out of labs yet (so not part of the actual release) if that is what you mean. Things under In the meantime, we are investigating some internal core changes that will also improve Stay tuned! And feel free to play around with |
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: N/A
What is the new behavior?
Does this PR introduce a breaking change?
Other information