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

feat(engine): api to build custom elements #87

Merged
merged 8 commits into from
Jul 3, 2018
Merged

Conversation

caridy
Copy link
Contributor

@caridy caridy commented Feb 9, 2018

Details

This experimental PR allows the creation of custom elements that can be registered as web components from a LWCElement declaration. e.g.:

import { buildCustomElementConstructor } from "engine";
import LightningAvatar from "lightning-avatar";
const AvatarElement = buildCustomElementConstructor(LightningAvatar);
customElements.define('x-foo', AvatarElement);

Does this PR introduce a breaking change?

  • No

@jbenallen
Copy link

Is the sample code supposed to be this?

import { customElement } from "engine";
import LightningAvatar from "lightning-avatar";
const AvatarElement = customElement(LightningAvatar);
customElements.define('x-foo', AvatarElement);

@caridy
Copy link
Contributor Author

caridy commented Feb 9, 2018

good catch @deadpoetJBA, updated!

ArrayPush.call(attrs, attrName);
});
}
return class extends HTMLElement {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the current implementation, the constructor name will be HTMLElement. Since the compiler enforce the component class to be named, what about dynamically set the custom element class name to match the component class name? This would greatly improve the debugability.

export function customElement() {
    // ...
    let LwcCustomElement = class extends HTMLElement {
        // ...
    };

    Object.setProperty(LwcCustomElement, 'name', { value: Ctor.name });

    return LwcCustomElement;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this constructor is never used by the engine, it is only used by the DOM apis via the custom element regsitraion and instantiation mechanism, not need to add a name here.

@priand
Copy link

priand commented Feb 13, 2018

I ran several unsuccessful tests starting from scratch so the I decided to simply add a new component to the todo-mvc app with as less changes as possible. Here is my fork: https://git.soma.salesforce.com/priand/lwc-todo-mvc/tree/wc-components

  • I created a test-component in the todo directory
  • I added a reference to that component in app.html, to ensure that the component renderers properly
  • I added the custom element registration in main.js
  • I added a reference to that custom element in index.html
  • I 'yarn link' the lwc-engine from this pull request. Without that, the compilation fails as "customElement" is not exported by "engine". This is an easy way to know that the link is properly setup

Here is what I found so far:

  • If I use any name for the custom element (x-foo) then it fails registering because getCtorByTagName(tagName) in createVM() returned undefined
    main.js:1331 Uncaught TypeError: Cannot read property 'name' of undefined
    at createComponentDef (main.js:1331)
    at getComponentDef (main.js:1553)
    at createVM (main.js:2246)
    at new _a (main.js:2519)
    at main.js:3024
    at main.js:3026

  • If I name it like the LWC component, then it works with some issues.

  • The LWC component instance added to the app.html has its content added twice. Looks like a duplication due to both the LWC component and the custom element sharing the same name?
  • If, in main.js, I register the custom element before creating the App component, then it fails with the same 'Cannot read property 'name' of undefined'

@caridy
Copy link
Contributor Author

caridy commented Feb 13, 2018

@priand that's good inside, I know what I have to do! I will update this PR today.

@caridy
Copy link
Contributor Author

caridy commented Feb 13, 2018

@priand can you take it for another spin? I tried this locally, and it works fine for me now after fixing the LWC registration order. You just need to pull from the branch, run npm run build inside the lwc-engine folder, and you should be ready to roll.

@priand
Copy link

priand commented Feb 13, 2018

Ok, it works! I tried several initialization orders and they all worked. Good job!
The only remaining issue is when you name the custom element and the LWC component the same, then use the LWC component within another LWC component: its content is duplicated.

@caridy
Copy link
Contributor Author

caridy commented Feb 13, 2018

@priand yes, working to resolve that in another PR since it doesn't need to be part of the experimental, but a general fix all together.

@caridy
Copy link
Contributor Author

caridy commented Feb 13, 2018

@priand this PR #97 will fix the duplication issue. Once that's merged, I can update this branch with it, and you will be able to try this again.

@caridy caridy force-pushed the caridy/wc-customElement branch from 97e06f1 to 99bdbf8 Compare February 13, 2018 18:47
@caridy
Copy link
Contributor Author

caridy commented Feb 13, 2018

@priand can you try this again? it should be fine now to compose registered elements. No more duplication of content, and no need to load the same component twice.

import { Element } from "../main";
import { customElement } from "../wc";

describe('wc', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

notice that we can't add more tests here because jsdom doesn't support custom elements ATM.

@salesforce-best-lwc-internal
Copy link

Benchmark comparison

Base commit: 4b3b3d9 | Target commit: 99bdbf8

benchmark base(4b3b3d9) target(99bdbf8) trend
table-append-1k.benchmark:benchmark-table/append/1k 252.82 (± 2.78 ms) 249.41 (± 3.67 ms) 👌
table-clear-1k.benchmark:benchmark-table/clear/1k 13.40 (± 0.87 ms) 12.82 (± 0.41 ms) 👍
table-create-10k.benchmark:benchmark-table/create/10k 1400.51 (± 26.27 ms) 1449.51 (± 24.23 ms) 👎
table-create-1k.benchmark:benchmark-table/create/1k 151.63 (± 2.28 ms) 152.10 (± 2.96 ms) 👌
table-update-10th-1k.benchmark:benchmark-table/update-10th/1k 139.18 (± 3.43 ms) 143.90 (± 2.46 ms) 👎
tablecmp-append-1k.benchmark:benchmark-table-component/append/1k 321.26 (± 4.48 ms) 324.43 (± 6.81 ms) 👌
tablecmp-clear-1k.benchmark:benchmark-table/clear/1k 31.87 (± 1.01 ms) 32.52 (± 1.38 ms) 👎
tablecmp-create-10k.benchmark:benchmark-table-component/create/10k 2108.97 (± 18.99 ms) 2116.11 (± 79.80 ms) 👌
tablecmp-create-1k.benchmark:benchmark-table-component/create/1k 238.71 (± 4.47 ms) 294.54 (± 7.76 ms) 👎
tablecmp-update-10th-1k.benchmark:benchmark-table-component/update-10th/1k 136.13 (± 3.88 ms) 167.66 (± 17.30 ms) 👎

@priand
Copy link

priand commented Feb 14, 2018

@caridy Thanks Caridy! A quick test this morning shows that there is no longer duplicated content.

@priand
Copy link

priand commented Feb 14, 2018

I created a library that exports some simple components and consume them in applications (basic html, Angular...). (internal link removed)

The web components run properly from the testapp, defined in the library project, but not from the external apps consuming the library. At runtime, the engine complains about the "key" while there is no iterator involved. It is not clear to me if this is an issue with the way the components are packaged (using the iife version here) or an issue with the engine.

Uncaught Error: Assert Violation:  <h3> "key" attribute is invalid or missing for [object:vm Header (1)]. Key inside iterator is either undefined or null.
    at Object.isTrue (webpack-internal:///../../../../../../components-library/dist/lwc-components.iife.js:39)
    at h (webpack-internal:///../../../../../../components-library/dist/lwc-components.iife.js:1018)
    at tmpl$1 (webpack-internal:///../../../../../../components-library/dist/lwc-components.iife.js:3559)
    at evaluateTemplate (webpack-internal:///../../../../../../components-library/dist/lwc-components.iife.js:1363)
    at invokeComponentRenderMethod (webpack-internal:///../../../../../../components-library/dist/lwc-components.iife.js:1431)
    at renderComponent (webpack-internal:///../../../../../../components-library/dist/lwc-components.iife.js:1589)
    at rehydrate (webpack-internal:///../../../../../../components-library/dist/lwc-components.iife.js:3245)
    at renderVM (webpack-internal:///../../../../../../components-library/dist/lwc-components.iife.js:3167)
    at HTMLElement.connectedCallback (webpack-internal:///../../../../../../components-library/dist/lwc-components.iife.js:3506)
    at HTMLDivElement.appendChild (webpack-internal:///../../../../../../components-library/dist/lwc-components.iife.js:3461)

@pmdartus
Copy link
Member

@deadpoetJBA Our master branch has a breaking change in the template compiler. Each virtual node generated has now a unique key property. I think linking lwc-compiler and lwc-template-compiler would solve your issue.

@priand
Copy link

priand commented Feb 15, 2018

@pmdartus That did the trick. Thanks Pierre-Marie.

@caridy
Copy link
Contributor Author

caridy commented Feb 15, 2018

@priand excellent! btw, remember this is a to-be-open repo, any link or internal information should not be posted in issues. I have updated your comment to remove the internal links.

@priand
Copy link

priand commented Mar 22, 2018

Hi @caridy - Are you going to merge this branch soon? I'm still using it but this makes me behind compared to master. Moreover, have you made some progress on the attributes as well as the content slots? Thanks!

@davidturissini
Copy link
Contributor

@priand There are no plans to merge this branch into master. However, I just merged master back into this branch so you can be up to date. LMK if you come across anything.

@priand
Copy link

priand commented Mar 22, 2018

Ok, thanks @davidturissini, I'll use that. By "there is no plan to merge this branch", do you mean that the creation of custom elements that can be used from other frameworks (we are using Angular) is now out of scope?

@davidturissini
Copy link
Contributor

@priand Using custom elements under the hood is something we want to do. What I mean is that there is no timeline for merging this branch right now and its unclear (to me, at least) when this would get merged.

@caridy
Copy link
Contributor Author

caridy commented Mar 23, 2018

We will definitely have to wait after 214 release.

@caridy
Copy link
Contributor Author

caridy commented May 31, 2018

@priand I have updated this PR to match the current master (0.22) which enables the usage of slots :), enjoy it!

@diervo
Copy link
Contributor

diervo commented Jun 25, 2018

Closing this PR since after talking to Philip we can get away with way simpler solution for now.

@diervo diervo closed this Jun 25, 2018
@priand
Copy link

priand commented Jun 25, 2018

@diervo there must be a misunderstanding here, as we currently use the custom elements. Our server actually generates a page that contains the custom tags and also use slots. Only the root element was using createElement(), but we found this morning a solution to not require that. Is there a way to get the code that enables the custom elements registry merged into the master branch?

@priand priand reopened this Jun 25, 2018
@diervo
Copy link
Contributor

diervo commented Jun 27, 2018

Lets have another sync, I really don't think you need any of this for your use case as far as I understood it the other day

@priand
Copy link

priand commented Jun 27, 2018

@diervo As of now, our JSP pages generate an hierarchy of custom elements. What we showed you last week was only the root element created using a JavaScript call. But we overcame our initial issue and we removed that piece of code: we are now fully creating the pages using custom elements.
Can we create the components using createElement() instead of using the markup? Probably, but it will be a lot more cumbersome, in particular because the JavaScript in the page markup doesn't have access to the Component classes packaged within an iife file. Somehow, the classes will have to be exported out of the file to be accessible from the page, which is similar to what the custom element registry does.
I'll be happy to chat if you have concerns with this approach.

@caridy
Copy link
Contributor Author

caridy commented Jun 29, 2018

I have discussed this with @diervo and we will be merging this soon. @davidturissini @ekashida let's try to clean this up and add some integration tests since unit tests can't run Custom Elements today, so we can merge this.

@caridy caridy force-pushed the caridy/wc-customElement branch from 0353057 to 23edbd1 Compare July 2, 2018 19:50
caridy added a commit that referenced this pull request Jul 2, 2018
@caridy caridy changed the title feat(engine): experimental api to create web components feat(engine): api to build custom elements Jul 2, 2018
Ctor = resolveCircularModuleDependency(Ctor);
const def = getComponentDef(Ctor);
if (process.env.NODE_ENV !== 'production') {
assert.isTrue(isUndefined(Ctor.forceTagName), `The experimental support for web components does not include the support for \`static forceTagName\` to "${Ctor.forceTagName}" declaration in the class definition for ${Ctor}.`);
Copy link
Contributor Author

@caridy caridy Jul 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

banning forceTagName since we can't really support this without the is attribute in safari.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will need to get ahead of this breaking change in core. Can you add this to #423 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No no, this is only for buildCustomElementConstructor's 2nd argument. This does not affect anything else. Since we always extends HTMLElement in this factory method, there is no way to force or use the is attribute whatsoever.

}
// adding all public descriptors to the prototype so we don't have to
// do it per instance in html-element.ts constructors
defineProperties(LightningWrapperElement.prototype, def.descriptors);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pmdartus @diervo this is what I mentioned this morning. It is an optimization when using WC, where all public APIs are defined in the proto via def.descriptors, which contains everything is needed to pipe values into the component instance.

@@ -152,15 +152,15 @@ export function removeVM(vm: VM) {
patchShadowRoot(vm, []);
}

interface ShadowRootInit {
export interface CreateVMInit {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ts was complaining that isRoot and fallback were not part of the standard ShadowRootInit interface.

@@ -92,7 +92,7 @@ export function evaluateTemplate(vm: VM, html: Template): Array<VNode|null> {
}

// TODO: add identity to the html functions
const { component, context, cmpSlots = EmptySlots, cmpTemplate } = vm;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pmdartus this is what I mentioned this morning, we were passing empty instead of undefined for native.

@@ -110,13 +110,18 @@ function LWCElement(this: Component) {
setInternalField(component, ViewModelReflection, vm);
setInternalField(elm, ViewModelReflection, vm);
setInternalField(cmpRoot, ViewModelReflection, vm);
let { elmProto } = def;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pmdartus this is the refactor that I mentioned this morning. The super fast pass is for real WC in which case we need to do nothing because those descriptors are part of the custom element constructor's prototype (detectable via PatchedFlag mark).

On the other hand, if it needs to be patched, we need have a fast path when the proto is the expected proto, in which case we do the proto chain mutation. But if we need to go on the slow path, we don't do proto, we just define the properties (which is what we were doing before anyways).

let tableElement;
let store;

before(async () => {
tableElement = createElement('benchmark-table', { is: Table });
tableElement = createElement('benchmark-table-component', { is: Table });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ambiguity on the name of the tests discussed in slack today.

@@ -0,0 +1,33 @@
import { buildCustomElementConstructor } from 'engine';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding a new set of benchmark tests for WC only.

@salesforce-best-lwc-internal
Copy link

Benchmark results

Base commit: 8514676 | Target commit: 39ebd59

@pmdartus
Copy link
Member

pmdartus commented Jul 3, 2018

Do we have integration test for this?
Also, I see that there is a test failure in the current integration test suite.

@caridy caridy force-pushed the caridy/wc-customElement branch from 39ebd59 to 7326a10 Compare July 3, 2018 14:50
@caridy
Copy link
Contributor Author

caridy commented Jul 3, 2018

@pmdartus

Do we have integration test for this?

@davidturissini has a whole new PR to run existing integration tests with the new mode. Still need some work with selenium and such. For now, getting the benchmark to run to completion seems like a good enough signal.

Also, I see that there is a test failure in the current integration test suite.

Seems like a flapper, running it again.

@salesforce-best-lwc-internal
Copy link

Benchmark results

Base commit: 8b26852 | Target commit: 7326a10

lwc-engine-benchmark

table-append-1k metric base(8b26852) target(7326a10) trend
benchmark-table/append/1k duration 156.30 (± 6.30 ms) 155.40 (± 5.30 ms) 0.58% 👌
table-clear-1k metric base(8b26852) target(7326a10) trend
benchmark-table/clear/1k duration 12.30 (± 0.90 ms) 12.65 (± 0.55 ms) -2.85% 👌
table-create-10k metric base(8b26852) target(7326a10) trend
benchmark-table/create/10k duration 930.60 (± 16.70 ms) 907.60 (± 12.00 ms) 2.47% 👍
table-create-1k metric base(8b26852) target(7326a10) trend
benchmark-table/create/1k duration 103.40 (± 2.80 ms) 103.80 (± 2.50 ms) -0.39% 👌
table-update-10th-1k metric base(8b26852) target(7326a10) trend
benchmark-table/update-10th/1k duration 92.80 (± 4.90 ms) 88.50 (± 4.90 ms) 4.63% 👌
tablecmp-append-1k metric base(8b26852) target(7326a10) trend
benchmark-table-component/append/1k duration 237.10 (± 4.60 ms) 231.10 (± 4.70 ms) 2.53% 👍
tablecmp-clear-1k metric base(8b26852) target(7326a10) trend
tablecmp-create-10k metric base(8b26852) target(7326a10) trend
benchmark-table-component/create/10k duration 1649.90 (± 12.30 ms) 1627.40 (± 8.70 ms) 1.36% 👍
tablecmp-create-1k metric base(8b26852) target(7326a10) trend
benchmark-table-component/create/1k duration 181.80 (± 4.70 ms) 171.40 (± 3.70 ms) 5.72% 👍
tablecmp-update-10th-1k metric base(8b26852) target(7326a10) trend
benchmark-table-component/update-10th/1k duration 80.20 (± 4.10 ms) 78.50 (± 3.70 ms) 2.12% 👍

@caridy caridy merged commit 614e8dd into master Jul 3, 2018
@caridy caridy deleted the caridy/wc-customElement branch July 3, 2018 18:30
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

Successfully merging this pull request may close these issues.

6 participants