Skip to content
This repository has been archived by the owner on Jan 18, 2022. It is now read-only.

feat: Vue 2.x compability #9

Closed
wants to merge 91 commits into from

Conversation

karol-f
Copy link

@karol-f karol-f commented Oct 8, 2016

Vue-element

Completely rewritten Vue-element plugin, full-featured, with docs and demos.

Docs - https://github.com/karol-f/vue-element/tree/wip/master/vue_2_element

Demos - https://karol-f.github.io/vue-element/ (GithubPages served from 'docs' folder)

Description

Vue-element is a tiny wrapper around Vue components. It provide seamless way to use it in HTML, plain JavaScript, Vue, React, Angular etc., using power of Custom Elements.

  • Works with Vue 0.12.x, 1.x and 2.x
  • Small - 2.5 kb min+gzip, optional polyfill - 5,1 kb min+gzip

Why you might need Vue-element?

Vue-element

It might be confusing for users to understand difference between Vue components, Custom Elements and it's use cases.

Why you might need Vue-element? Simply, for your Vue components user's convinience. All they would need to do is include your JavaScript file and then they can:

  • include HTML tag (e.g. <my-component><my-component />) in any time of document lifecycle. You can use your elements in e.g. SPA application just by including HTML tag - no Vue initialization or JavaScript usage is needed. Custom Elements will auto initialize when mounted into document. You can include them in e.g. Vue, Angular or React projects and browser will take care of detecting it and initialization
  • use simple API that allows for interacting with underlaying Vue instance by changing attributes, props or passing callback functions
  • take advantage of features like lazy-loading, that allows for loading components on demand, only when user add them to document

Features

  • Simplicity - only tag-name and Vue component object is needed for Vue.element() usage
  • Compatibility - using optional polyfill we can support wide range of browsers, including IE9+, Android and IOS
  • Full featured - you can use nesting, HMR, slots, lazy-loading, native Custom Elements callbacks.
    • reactive props and HTML attributes
    • automatic props casting (numbers, booleans) so they won't be available as strings but proper data types
    • passing callback functions to props via JavaScript
    • 'default' and 'named' slots are available, check demo for example
    • Hot Module Replacement for seamless developer experience (Vue 2.x+)
    • lazy-loading - you can download component after it's attached to document. Useful for e.g. UI library authors. Check demo for example
    • detecting if detached callback is not invoked due to opening vue-element in modal - element is then detached and attached to DOM again. It would be undesirable to destroy it immediately
  • Custom Elements v1 - compatible with latest specification. Vue-element will use native implementation if supported

It's rewritten using ES6, proper build process, prepared for future tests. Feel free to ask questions.

Regards.

@richardanaya
Copy link

richardanaya commented Oct 30, 2016

Holy shit, this is amazing! I can even nest them.

@danieldiekmeier
Copy link

If I read the source correctly, this doesn't support the shadow DOM. Is that on purpose?

@karol-f
Copy link
Author

karol-f commented Nov 5, 2016

@danieldiekmeier no, sorry, I've forgot to add it. I'll try to update this PR in the next days.

@richardanaya
Copy link

If possible would be cool if there was a way to get access to the inner
element Dom of custom element too

need this

On Nov 5, 2016 1:39 PM, "Karol Fabjańczuk" [email protected] wrote:

@danieldiekmeier https://github.com/danieldiekmeier no, sorry, I've
forgot to add it. I'll try to update this PR in the next days.


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
#9 (comment), or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAR8mu5a56354dYpCk7lTDxKt5B8TYUrks5q7M1zgaJpZM4KRkkL
.

@dariuszsikorski
Copy link

dariuszsikorski commented Dec 10, 2016

That's a huge step towards vue 2.0 support, i tried it with some rewriting to vue 2 syntax and it works, but seems it doesn't support <slot> tag inside element, and I wasn't able to reproduce google maps webcomponent with following structure:

<body>
  <google-map>
    <map-marker></map-marker>
    <map-marker></map-marker>
    <map-marker></map-marker>
  </google-map>
</body>

map-markers would be placed in place of <slot> in this case.

docs: https://vuejs.org/v2/guide/components.html#Single-Slot

@karol-f
Copy link
Author

karol-f commented Dec 10, 2016

@dariuszsikorski I would suggest creating wrapping vue-element with template with google-maps and map-marker, which will be simple vue components.

@dariuszsikorski
Copy link

dariuszsikorski commented Dec 10, 2016

@karol-f What you suggest is to move <map-marker> tags into <google-map>'s template: "..." definition. I would like to dynamically add/remove markers with typical javascript, like $('google-map').append('<map-marker />');, so involving vue syntax, is something I'd like to avoid. Custom elements should expose pure new tags to a browser without defining <template>, <script type="x-template"> or template: '...' for each <google-map> tag which has different markers inside.

@karol-f
Copy link
Author

karol-f commented Dec 10, 2016

@dariuszsikorski I think your requirements are valid. Unfortunately in at least two weeks time I probably won't be able to try to implement it. I'll remember about it and try to implement it along with migration to v1 spec of WebComponents and HMR reload. Thanks.

@johnjunpeng
Copy link

@karol-f I'm trying to use your Vue.element, but not getting what I need. My first question is from other global javascript code, how to retrieve and use the reference of a vue component inside your vue element? If I have , then ....$refs.myvueelement seems to return dom node directly which behaves differently than regular Vue.component. Also, I specified shadow:true in options, but saw no effect. I did see a new attr ve-ready like <myvue-element ... ve-ready> when I inspect. Appreciate your help.

@karol-f
Copy link
Author

karol-f commented Jan 31, 2017

@johnjunpeng Hi, I'll try to answer:

1). Reference to vue component is held in __vue_element__ prop (as stated in https://github.com/karol-f/vue-element/tree/wip/master/vue_2_element#testing). So in your case it should be in $refs.myvueelement.__vue_element__

2). About shadow: true - it will work only if browser supports it - check it in e.g. newest Chrome. In my brief test it creates shadow dom. I didn't test it further.

As stated in https://www.webcomponents.org/polyfills#shadow-dom-polyfill it's hard to polyfill that and using alternatives is preferred

3). ve-ready is just a helper attribute so you can style it in CSS like

[ve-ready] {
    background-color: blue;
}

Regards!

@johnjunpeng
Copy link

johnjunpeng commented Jan 31, 2017

@karol-f Thanks a lot for the quick response. I tried your way using __vue_element__ and it seems to be working in the latest Chrome 55. The syntax is below, a bit awkward though having to use $children, but I couldn't think of any other way.
vueComponents.$refs.iviewertimershadow.__vue_element__.$children[0].startTimer(...);
But the exact same code did not work in latest firefox or my Electron Browser (Chrome 51 based), both gave [Vue warn]: Unknown custom element: <iviewer-timer-shadow> - did you register the component correctly? For recursive components, make sure to provide the "name" option. (found in root instance). I remember someone else also reported the same msg earlier.
Also, still I haven't see shadow-root being create in the latest chrome when I specified shadow: true

Thanks again and best regards!

John

@karol-f
Copy link
Author

karol-f commented Jan 31, 2017

@johnjunpeng Hi

Regarding triggering method on child component. Wouldn't it be easier to trigger to trigger it just by changing attribute or prop? Check example - https://jsfiddle.net/zL4zxz7e/. As you can see, method is triggered by using watch in vue component passed to vue-element. It think it's cleaner approach.

In your approach it should be also doable.

About [Vue warn]: Unknown custom element - it's explained in https://github.com/karol-f/vue-element/tree/wip/master/vue_2_element#caveats. It is known and desirable behaviour as you want element to be catched by browser, not Vue itself.

Regarding Shadow Dom - Probably you didn't put it into third parameter of Vue-element but second one. I've added it to JS Fiddle mentioned above. In Chrome I can see that element has #shadow-root (open) before inner HTML.

Regards

@johnjunpeng
Copy link

johnjunpeng commented Feb 1, 2017

@karol-f Thanks again for the details. Yes, it was my bad, I didn't pass shadow: true as the 3rd parameter, but 2nd one as you said. I should have read your documentation more carefully. So shadow-root is created and working properly in Chrome. Your simple example on jsfiddle is very helpful and works in both chrome and firefox. But somehow I could not make my code work in firefox (upon dom inspection, I saw empty element <iviewer-timer-shadow></iviewer-timer-shadow>) probably due to document.registerElement(...) is not supported in Firefox, but vue-element should have implement a fallback mechanism in this case since your example works in firefox. I am puzzled - maybe because there is a vue-component inside my vue-element (template: '<iviewer-timer ref="iviewertimer"></iviewer-timer>'). I agree changing attr/prop is usually a better approach to trigger it from outsider. But my case is a little complicated since I want to pass in a callback function to the method of a vue-element which is not feasible or making sense via a attr/prop. Also I just wonder how expensive is watch mechanism, maybe a direct method call is more efficient than watch if many places have such a need. Lastly can you please confirm the following statements (from an old post for the previous version of vue-elment for Vue 1.x) are still true for the latest version of vue-element for Vue 2.0? I appreciate it!

Usage for Vue.element() is the same as Vue.component() - you pass in exactly the same options as if you are defining a Vue component. A few things to note:

Nested Vue custom elements are not supported - it is recommended to use Vue's own component system inside a custom element; The custom element interface is intended for inter-op with other libraries (e.g. Polymer).

You don't need to manually instantiate a root level Vue instance. Custom Elements get auto-promoted when document.registerElement is called. You can also freely define the element before or after the markup.

Real custom elements must contain a hyphen in its tag name. For example, my-element is valid, but myelement is not.

You can expose attributes with Vue's props (0.12) or paramAttributes (0.11) option, but you can only pass in literal values (no dynamic bindings). See the example folder to see it in action.

By default the element does not use Shadow DOM. If you want to enable Shadow DOM encapsulation, pass in shadow: true in your component options.

@karol-f
Copy link
Author

karol-f commented Feb 1, 2017

@johnjunpeng

About your specific use case, when you want trigger callback from child. It would be much easier if you prepare JS Fiddle so I can check it. Thanks.

Regards!

@karol-f
Copy link
Author

karol-f commented Feb 1, 2017

@yyx990803 Can we decide what to do with this PR? I suggest two options:

  1. This PR will be accepted and I will be responsible for maintaining it, issue solving and communicating (replying to questions etc.)

  2. I will create separate repo for this project (independent from Vue) so you can focus on Vue and won't be distracted by this project.

What should we choose?

@johnjunpeng
Copy link

johnjunpeng commented Feb 1, 2017

@karol-f Thank you again! I tried your polyfill using <script src="https://cdnjs.cloudflare.com/ajax/libs/document-register-element/1.3.0/document-register-element.js"></script> and it works for Firebox but not my IE 11. Haven't tried your recommended way for passing callbacks to props, I need to study your code example.

Best Regards!

@karol-f
Copy link
Author

karol-f commented Feb 1, 2017

@johnjunpeng Please check if your app works with IE11 without Vue-element, because document-register-element polyfill is all that is needed for Vue-element. If it's not working, maybe your app uses unpolyfilled Promises etc?

As you can see in Vue-element demo (https://karol-f.github.io/vue-element/) it's working even with IE9.

Regards.

@johnjunpeng
Copy link

In IE 11, I got the below error
Error occurred while calling createTimers() TypeError: Unable to get property '$children' of undefined or null reference which is related to the below code
vueComponents.$refs.iviewertimershadow.__vue_element__.$children[0].startTimer(...);. Looks like __vue_element__ had a problem in IE 11.

Thanks!

@karol-f
Copy link
Author

karol-f commented Feb 1, 2017

@johnjunpeng As you see in demo it's working even in IE9. Please create JS Fiddle or it will very hard to nail the problem.

One thing that comes to my mind is that in IE you have to check $refs in mounted callback after $nextTick or it will be empty.

Regards.

@johnjunpeng
Copy link

OK, thanks. I'll try but it may take me a while to create a code snippet to demo my case on JS Fiddle. Sorry, I am new to Vue, but what is mounted callback? Do you have links for documentation and code example how to use it? Appreciated your help!

@karol-f
Copy link
Author

karol-f commented Feb 1, 2017

@johnjunpeng Here you are https://vuejs.org/v2/guide/instance.html#Lifecycle-Diagram.
I suggest walking trough documentation, maybe vue-element isn't needed in your case. Regards

@johnjunpeng
Copy link

johnjunpeng commented Feb 1, 2017

@karol-f Thank you very much! I've found the root cause of my issue in IE 11. The performance of the whole thing is much slower in IE than Chrome and Firefox. (Your js fiddle example was also displayed much slower in IE than Chrome and Firefox) Therefore vueComponents.$refs.iviewertimershadow.__vue_element__.$children[0].startTimer(...);. was called too early in my application logic, before Vue finished initialization, thus giving undefined $children error. After I adjusted the timing in my logic, vue-element polyfill now works in IE 11.

@johnjunpeng
Copy link

johnjunpeng commented Feb 1, 2017

@karol-f I found out mounted callback is executed before vueComponents.$refs.iviewertimershadow.__vue_element__.$children is ready to be used for IE 11 at least. Is this true? I am forced to write the below code with window.setTimeout() in order for vue element works in IE 11. Your insights are much appreciated.

Vue.element("iviewer-timer-shadow", { template: '<iviewer-timer ref="iviewertimer"></iviewer-timer>', methods: { setTimer: function(timerForValue, timerLabelValue, totalTimeInSeconds) { this.$refs.iviewertimer.setTimer(timerForValue, timerLabelValue, totalTimeInSeconds); }, startTimer: function (timerEndCallback) { this.$refs.iviewertimer.startTimer(timerEndCallback); } }, mounted: function() { setTimeout( function() { iViewer.vueComponentReady(true); }, 500 ); } }, { shadow: true });

@karol-f
Copy link
Author

karol-f commented Feb 2, 2017

@johnjunpeng it should work too:

function mounted() {
  Vue.nextTick(() => {
    //...
  });
}

Regards!

@johnjunpeng
Copy link

@karol-f Vue.nextTick() indeed solved this problem on IE. Unfortunately I retested this same code on Firefox and found out it still suffered the same issue: vueComponents.$refs.iviewertimershadow.__vue_element__.$children is NOT ready even after mounted: function() { Vue.nextTick(...) } was executed especially when running code against my node server.
Any other ways to try? I really hate to put my setTimeout logic in the code since it's only a hack.
Thanks again!

@karol-f
Copy link
Author

karol-f commented Feb 2, 2017

@johnjunpeng I would love to help but this PR is not a place to solve your problem. Please create issue on my repo and prepare JS Fiddle showing problem and describing what you want to achieve. Regards!

@yyx990803 Please respond to #9 (comment) so we can finally close this PR and decide what to do next. I know that you are busy man so a lot of comments in this thread don't help, sorry about that.

@tomByrer
Copy link

tomByrer commented Feb 4, 2017

@karol-f You might have to email/Twitter-message him directly, seems Evan shut off notifications. Maybe wait to Tuesday just in case.

@HerringtonDarkholme
Copy link
Member

Thanks @karol-f for your enthusiastic contribution! But I'm quite concerned about many semantic issues and library distribution issues which need more discussion:

  1. how should Vue's event emitted? Vue-element does permit callback as props. But I would expect a custom element will provide an interface using addEventListener and dispatchEvent to outside. Something like this

  2. For now it seems all component is extending HTMLElement. How about make component extend its root element? For example, a component with template <img :src="src" /> would extend HTMLImage, it resembles current Vue runtime since no more wrapper tag is used in component instantiation.

  3. Currently vue-element is a runtime library. How about integrated into vue-loader/vueify so we can provide a custom element with minimal runtime overhead?

  4. Vue-element of course depends on Vue. If users import vue-element from multiple parties, how can we avoid duplicate Vue library code?

Every aspect requires design decision. I'm quite afraid that one decision change could force whole implementation rewrite.

@karol-f
Copy link
Author

karol-f commented Feb 6, 2017

Hi @HerringtonDarkholme

  1. Nice idea. I also thought about it and I will add it to my TODO list!
  2. I think there are few issues with it. For now, custom element tag in HTML is used for keeping reference to Vue instance (__vue_element__), passing props, attributes and callbacks. Also, looking at 1., it can be HTML node for listening for events. If we will replace it with passed Vue component we will loose this functionality. But when we will not replace it, we end up will custom element extending HTMLImage AND <img :src="src" /> from template.
    Do I miss something?
  3. Custom Elements specification enforce us to register tags in runtime. Also when tag is detected, browser runs it's callbacks during runtime. We cannot bypass it. Vue-element is designed for use cases like e.g. Onsen UI - bunch of custom elements that can be used anywhere. If you want to make it more performant in your use case, e.g. when using with Vue, just register it with Vue.component instead. Also you can always pass to vue-element component compiled by vue-loader e.g. from *.vue files. It will work so performance wouldn't be compromised. So runtime is only for registering Custom Element. Vue component might be compiled.
  4. It's up to Vue-element component's authors to expose components in such way that Vue could be passed in. This is good case and maybe I should prepare example of it.

Thanks for good questions!
Regards.

@yyx990803
Copy link
Member

Hey @karol-f , thanks again for the effort.

First, the decision: I think we should deprecate the current repo and let you create a separate repo for this.

The general idea is that anything under the vuejs organization is fully endorsed and officially maintained by the core team. Unless otherwise noted, it implies that the team is committed to the project and responsible for potential issues and required fixes.

However, the significant changes you made in this PR makes it essentially a completely new project - and more importantly, your project. I really think it's better for you to take ownership.

To be clear, we'd love to collaborate with you to improve the project so that it becomes the de-facto solution for Vue + Custom Element integration, but it doesn't necessarily mean it must be hosted in the vuejs organization.


Now for the feedback - @HerringtonDarkholme raised a bunch of good questions. Here are some of mine:

  • We need proper tests for this. It seems there are no tests at the moment?

  • The 3000ms detach check still feels a bit hacky to me. We might want to make this explicitly documented and provide an option to configure it.

  • source code structure wise: I prefer moving the demo code out of src.

  • I think the primary use case for Vue + Custom Element is distribution. The ideal workflow would be a CLI tool that allows the user to simply compile a *.vue file into a ready-to-use custom element. However, there are quite a few unresolved issues that we need to figure out:

    • The compiled version of the component will have to include vue-element runtime, so the file size is crucial. I'd suggest using Rollup + Buble for smaller dist file size (see vue-router/vuex build setup). There's no need to detect ES2015 - just use the same class syntax and let Buble transpile it - the non-class extension works the same way. Also, we can remove compatibility code for Vue versions below 1.

    • We will implement an alternative compilation mode for vue-loader that instead of relying on vue-style-loader, uses some runtime API provided by vue-element to insert a CSS string as a <style> tag into the custom element's Shadow DOM. This requires vue-element to provide a css option:

    Vue.element('my-foo', ComponentOptions, {
      css: '...' // CSS string inserted as <style> into the shadow root
    })
  • Finally, an extra flag in vue-cli that brings everything together:

    vue build --custom-element MyComponent.vue
    

@yyx990803 yyx990803 closed this Feb 7, 2017
@karol-f
Copy link
Author

karol-f commented Feb 26, 2017

If anyone is interested, Vue-element was renamed to Vue-custom-element and published to https://github.com/karol-f/vue-custom-element repository.

@HerringtonDarkholme
1). Events are added. Thanks for the idea. I think it's more useful than callbacks. Check demo on https://karol-f.github.io/vue-custom-element/#/demos/events

@yyx990803

  • test are on my TODO list and will be implemented soon
  • the 3000ms detach is now configurable - user can set it to 0 if he/she want
  • Demo is now on separate folder.

Distibution

I'd suggest using Rollup + Buble

I've tested Webpack@1 (7,7 Kb), Webpack@2 (8,1 Kb) and Rollup (7 Kb). Indeed, Rollup filesize was smallest so dist/vue-custom-element.js is now compiled using Rollup.

There's no need to detect ES2015 - just use the same class syntax and let Buble transpile it

Unfortunately it's not that simple. We cannot simply extend native HTML elements (babel/babel#4480). Due to that I use https://github.com/github/babel-plugin-transform-custom-element-classes which uses Reflect.construct. Reflect.construct polyfill weights probably more then Vue-custom-element library so it's applied only if browser natively supports it. So we have:

  • Class extend HTMLElement if browser supports ES2015 - thanks to it will work even without polyfill if browser supports Custom Elements
  • fallback for older browsers, without use of heavy Reflect.construct polyfill

It's desirable as Vue-custom-element will run even without polyfill but fallback to it for older browsers.

We can remove compatibility code for Vue versions below 1

Compatibility with versions < 1 is done with 7 lines of code. Gzip'ped difference is 0.04 Kb. Do you think it's worth it?

We will implement an alternative compilation mode for vue-loader

Wow, that's a nice idea. I will need some help with that though. I'll start an 'issue' with it (karol-f#2) and we can implement it.

Thanks and regards!

@karol-f karol-f deleted the wip/master/vue_2_element branch February 27, 2017 19:16
@ennovum
Copy link

ennovum commented Oct 2, 2017

@yyx990803 TL;DR is the Vue core team looking into embracing the idea of creating native custom elements with Vue?

I'm currently conducting a broad research which should result in a recommendation for my organization. Because we're building a platform for software development, the recommendation may result in choosing a technology for a number of applications in at least several years to come.

We did a similar choice ~3 years ago when we planned a PoC application for the platform. We chose Angular JS. For some time we were very happy with the framework. We delivered features with speed and quality. But then Angular 2 happened. I'm sure I don't need to tell you that the stakeholders are not happy about where we end up with our initial choice. Even though I can explain the stakeholders what happened to Angular JS and why it happened, it does not make them less worried about the future. All they ask now is how we can minimize the risk of this same thing happening to the technology we choose this time. I can't blame them.

Usually when developers look for business stability they look for corporate backing and community strength. This is valid. But to me the most underestimated point here is the spec compliance. For this reason I chose ES6 over CoffeeScript once and last couple of years did prove it a much better choice. As of now, web components are emerging and since they are a part of the spec, I'm strongly leaning towards incorporating them in some way in our technology stack.

In the past few days I've been looking at how modern web frameworks intersect with native web components. Obviously I came across vue-custom-element project. While Vue can be considered stable businesswise as there is some corporate backing (and rumors about even more serious backing) and the community is very strong, the vue-custom-element still falls into the pet project category to me. The work done there is amazing and we all owe many thanks to @karol-f , but without the core team support, it can't be considered serious enough, at least by me.

This PR was closed and I did see the explanation. But maybe the decision had something to do with Vue 2 being in progress. Or some other plans for web components compliance. I'm sure there are more people like me trying to make safe choices but at the same time work with new exciting technologies.

So again, is the Vue core team looking into embracing the idea of creating native custom elements with Vue? Is there a roadmap and a timeline?

Thanks for getting all the way down here.
Best regards.

@karol-f
Copy link
Author

karol-f commented Oct 2, 2017

@ennovum I feel the same. Choosing Web Components (for me Custom Elements and maybe Shadow Dom) is the key for future-proof rewrites. My comment for Gutenberg project seeking for new technical fron-end stack - WordPress/gutenberg#2733 (comment)

In short I think that wrapping any framework with Custom Element and communicating using standard non-changable API is future-proof as you can change your underlying FE technology one component at the time. IMHO It's something like microservices for frontend.

@yyx990803
Copy link
Member

What we will likely do: provide a CLI option that compiles Vue components directly into distributable custom elements (similar to stencil.js).

But overall I still see the value of WC being more about interop / distribution rather than the "safety bet" many seem to think of it as. WC alone gives you too little for meaningful app development, even with WC being the underlying composition unit, you will still need a higher-level framework like Polymer, Stencil or Vue to support actual development. At that level you are back to ground zero when making the decisions, WC simply becomes the new baseline.

@ennovum
Copy link

ennovum commented Oct 2, 2017

@yyx990803 thanks for your reply.

To me web components as the new baseline is not nothing. And I expect the spec of web components to grow and give us more and more features in time. So if I knew that a framework's architect is into embracing the standards and building with them in mind, I'd be more eager to put my trust into this framework being the safe choice I need.

Is there even a vague date to the CLI option you mentioned?

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

Successfully merging this pull request may close these issues.

10 participants