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

Carbon v11 #1587

Open
benceszenassy opened this issue Apr 11, 2024 · 14 comments
Open

Carbon v11 #1587

benceszenassy opened this issue Apr 11, 2024 · 14 comments
Milestone

Comments

@benceszenassy
Copy link
Contributor

Is your feature request related to a problem? Please describe.
We should keep up, with the design kit, react and angular components.

Describe the solution you'd like
On a separate branch, we should update the components incrementally, based on the react components.

Additional context
There are lot of basic features missing from v10 like readonly form fields, toasts that remove them selves after x ms, etc.

@davidnixon I'm gladly participating in this update, if we can do it.

@davidnixon
Copy link
Collaborator

#1596
LMK what you think of wrapping web components.
The HUGE advantage is that we have a large pool of maintainers on the web components team.

@jeffchew I wrapped cds-button and cds-text-input. Those work pretty well. I would still like to try and generate wrappers automatically so I'll keep exploring that.

@benceszenassy
Copy link
Contributor Author

benceszenassy commented May 1, 2024

I didn't play too much with web components, the first thing that pops in my mind, is scoped slots, but i think we can workaround it with provide / inject.

There are also some areas where we find custom elements to be limiting:
Eager slot evaluation hinders component composition. Vue's scoped slots are a powerful mechanism for component composition, which can't be supported by custom elements due to native slots' eager nature. Eager slots also mean the receiving component cannot control when or whether to render a piece of slot content.
Shipping custom elements with shadow DOM scoped CSS today requires embedding the CSS inside JavaScript so that they can be injected into shadow roots at runtime. They also result in duplicated styles in markup in SSR scenarios. There are platform features being worked on in this area - but as of now they are not yet universally supported, and there are still production performance / SSR concerns to be addressed. In the meanwhile, Vue SFCs provide CSS scoping mechanisms that support extracting the styles into plain CSS files.

vuejs.org

Other than that i think its a good direction.

I ran through your commits, i like the idea of using vite.
It now seems like a full rewrite so i would introduce typescript too, i dont think it will be too much of a burden.

For wrapper generation, lit labs have something like that, but i didn't tried it yet - gen-wrapper-vue

@benceszenassy
Copy link
Contributor Author

benceszenassy commented May 25, 2024

Hey @davidnixon, i played a little with this lib, its not near an automated wrapper generator, but it can definitely can generate types for props and such.

I checked out the carbon repository, and wired-in the lib.

package.json

...
  "scripts": {
    "generate-vue-types": "custom-elements-manifest analyze && node generate-vue-types.js",
...
  "devDependencies": {
    "@custom-elements-manifest/analyzer": "^0.10.2",
    "custom-element-vuejs-integration": "^1.2.0",
...

generate-vue-types.js

import { generateVuejsTypes } from 'custom-element-vuejs-integration';
import manifest from './custom-elements.json' assert { type: 'json' };

const options = {
  outdir: './',
  fileName: 'vue-types.d.ts',
  componentTypePath: (name, tag) => `./src/components/${tag}/index.ts`,
};

generateVuejsTypes(manifest, options);

Generated custom-elements.json:
custom-elements.json

Generated vue-types.d.ts (d.ts upload not supported):
vue-types.txt

Its not exporting its types, but i think for a starter its better than nothing. There are libs for VSCode and JetBrains custom html and css json generations too.

And there is a lib for react component generation, maybe it can help to create a vue component generator? custom-element-react-wrappers

Copy link

This issue has been marked as stale because it has required additional
info or a response from the author for over 14 days. When you get the
chance, please comment with the additional info requested.
Otherwise, this issue will be closed in 14 days.

@github-actions github-actions bot added the stale 🍞 No recent activity label Jun 10, 2024
@davidnixon
Copy link
Collaborator

@benceszenassy I have also been looking at tools and I found https://github.com/open-wc/custom-elements-manifest which I think is the same tool you referece above but a different git repo? And this one https://www.npmjs.com/package/custom-element-jet-brains-integration (I use WebStorm).

So I was able to generate this json
carbon-web-components-web-types.json

And from that I generated a VERY simple component for the copy button:

<template>
  <cds-copy-button v-bind="props">
    <slot> Copy to Clipboard </slot>
  </cds-copy-button>
</template>
<script setup>
import '@carbon/web-components/es/components/copy-button/index.js';
import { useSlots } from 'vue';
const props = defineProps({
  /** Specify an optional className to be added to your Button */
  buttonClassName: { type: String },
  /** `true` if the button should be disabled. */
  disabled: { type: Boolean },
  /** Specify the string that is displayed when the button is clicked and the content is copi */
  feedback: { type: String, default: 'Copied!' },
  /** The number in milliseconds to determine how long the tooltip should remain. */
  feedbackTimeout: { type: Number, default: 2000 },
  /** Focuses on the first focusable element in the shadow DOM. */
  focus: { type: String },
});
</script>

The generateReactWrappers is VERY interesting. I failed for some of the dot com components but it worked enough to look promising. I'll look at it this week.

I also looked at this one which did not really work at all https://www.npmjs.com/package/@lit-labs/gen-wrapper-vue

@davidnixon
Copy link
Collaborator

I think the comment above will remove the stale label when the action rus again.

@benceszenassy
Copy link
Contributor Author

benceszenassy commented Jun 14, 2024

@davidnixon i thinkered with it too, here is a gist with the generator (its not complete, but it can generate the basics) and with one example of a generated component.

https://gist.github.com/benceszenassy/7c06de0043e2dfe937008477489d24eb

With emits i dont know what would be the best practice, they didnt get any type for payload... we should create x amount of v-on handler with any as payload type, and simply emit with each?
There are custom types in some of the props, those cant be generated, the custom-elements.json cant pick up it well.

@davidnixon davidnixon removed the stale 🍞 No recent activity label Jun 18, 2024
Copy link

github-actions bot commented Jul 7, 2024

This issue has been marked as stale because it has required additional
info or a response from the author for over 14 days. When you get the
chance, please comment with the additional info requested.
Otherwise, this issue will be closed in 14 days.

@github-actions github-actions bot added the stale 🍞 No recent activity label Jul 7, 2024
@davidnixon davidnixon removed the stale 🍞 No recent activity label Jul 11, 2024
@benceszenassy
Copy link
Contributor Author

@davidnixon I dont have much time to play with the generator, it require more time to build one of these, than convert every component by hand imho.

What will be the vision for vue components? It should follow the react components functionality, or the web components? Should built from scracth or use the web components? If it is from web components what should reactivity be like? Or emitted event typing?

Im more comfortable with vue from scratch than built a web component wrapper library, but i still want to use this library in work, so if you can tell me the way, i can contribute to it.

@davidnixon davidnixon added this to the carbon v11 milestone Aug 22, 2024
@benceszenassy
Copy link
Contributor Author

@davidnixon any update on this?

@davidnixon
Copy link
Collaborator

@benceszenassy Yes, I mostly looked at the JetBrains stuff which was helpful but incomplete. It looks like you found the same. I got stuck on not being able to fully generate a component but being able to generate a skeleton of a component. I think you found the same?

Current sticking points for we were the slots. These were not behaving correctly at all for me.

I am still most interested in wrapping the web components:

  • I think we need a working pattern for slots
  • and a pattern for the emits issue you identified above
  • also need a pattern for what should be provided via v-model
  • as for reactivity, I think all the props passed to the web components should be reactive

@benceszenassy
Copy link
Contributor Author

benceszenassy commented Sep 17, 2024

@davidnixon i had time on my hands today, so a looked into it - these are examples from the generator i played locally (https://github.com/benceszenassy/cem-tools/tree/main | https://github.com/benceszenassy/carbon-components-vue/tree/carbon11)

slots

custom-elements doesn't have scoped slots, so i think its safe to say, that the geneator can handle the slot generation.

<template>
  <cds-textarea class="cv-textarea" v-bind="props">
    <template v-slot:helper-text>
      <slot name="helper-text" />
    </template>
    <template v-slot:label-text>
      <slot name="label-text" />
    </template>
    <template v-slot:validity-message>
      <slot name="validity-message" />
    </template>

    <slot></slot>
  </cds-textarea>
</template>

<script setup lang="ts">
import "@carbon/web-components/es/components/textarea/textarea.js";
import type { CvTextareaProps } from "./CvTextarea.ts";
const props = defineProps<CvTextareaProps>();

const emits = defineEmits<{
  // undefined
  (e: "invalid", value: CustomEvent): void;
}>();
const slots = defineSlots<{
  // The helper text.
  "helper-text": (scope: any) => any;
  // The label text.
  "label-text": (scope: any) => any;
  // The validity message. If present and non-empty, this input shows the UI of its invalid state.
  "validity-message": (scope: any) => any;
}>();
</script>

Or we can add scope, just for our own slot handling.

<template>
  <cds-textarea class="cv-textarea" v-bind="props">
    <template #[:helper-text]="scope">
      <slot name="helper-text" v-bind="scope" />
    </template>
    <template #[:label-text]="scope">
      <slot name="label-text" v-bind="scope" />
    </template>
    <template #[:validity-message]="scope">
      <slot name="validity-message" v-bind="scope" />
    </template>

    <slot></slot>
  </cds-textarea>
</template>

<script setup lang="ts">
import "@carbon/web-components/es/components/textarea/textarea.js";
import type { CvTextareaProps } from "./CvTextarea.ts";
const props = defineProps<CvTextareaProps>();

const emits = defineEmits<{
  // undefined
  (e: "invalid", value: CustomEvent): void;
}>();
const slots = defineSlots<{
  // The helper text.
  "helper-text": (scope: any) => any;
  // The label text.
  "label-text": (scope: any) => any;
  // The validity message. If present and non-empty, this input shows the UI of its invalid state.
  "validity-message": (scope: any) => any;
}>();
</script>

Or use a single runtime cycle.

<template>
  <cds-textarea class="cv-textarea" v-bind="props">
    <template v-for="(_, slot) of $slots" #[slot]="scope">
      <slot :name="slot" v-bind="scope" />
    </template>
  </cds-textarea>
</template>

<script setup lang="ts">
import "@carbon/web-components/es/components/textarea/textarea.js";
import type { CvTextareaProps } from "./CvTextarea.ts";
const props = defineProps<CvTextareaProps>();

const emits = defineEmits<{
  // undefined
  (e: "invalid", value: CustomEvent): void;
}>();
const slots = defineSlots<{
  // The helper text.
  "helper-text": (scope: any) => any;
  // The label text.
  "label-text": (scope: any) => any;
  // The validity message. If present and non-empty, this input shows the UI of its invalid state.
  "validity-message": (scope: any) => any;
}>();
</script>

emits

For example the CDSTabs use events from CDSContentSwitcher through a mixin (HostListenerMixin), i think the type data lost on the generic class generation, but if we had the information, in this example, the data that travels with the event is a reference for an HTML element, we can only extract it (blindly without types), and emit it further.

// content-switcher.ts
 /**
  * Handles `click` event on content switcher item.
  *
  * @param event The event.
  * @param event.target The event target.
  */
 protected _handleClick({ target }: MouseEvent) {
   const currentItem = this._getCurrentItem(target as HTMLElement);
   this._handleUserInitiatedSelectItem(currentItem as CDSContentSwitcherItem);
 }

 /**
  * Handles user-initiated selection of a content switcher item.
  *
  * @param [item] The content switcher item user wants to select.
  */
 protected _handleUserInitiatedSelectItem(item: CDSContentSwitcherItem) {
   if (!item.disabled && item.value !== this.value) {
     const init = {
       bubbles: true,
       composed: true,
       detail: {
         item,
       },
     };
     const constructor = this.constructor as typeof CDSContentSwitcher;
     const beforeSelectEvent = new CustomEvent(constructor.eventBeforeSelect, {
       ...init,
       cancelable: true,
     });
     if (this.dispatchEvent(beforeSelectEvent)) {
       this._selectionDidChange(item);
       const afterSelectEvent = new CustomEvent(constructor.eventSelect, init);
       this.dispatchEvent(afterSelectEvent);
     }
   }
 }
 
 /**
  * The name of the custom event fired after a a content switcher item is selected upon a user gesture.
  */
 static get eventSelect() {
   return `${prefix}-content-switcher-selected`;
 }
// tabs.ts
  /**
   * The name of the custom event fired after a a tab is selected upon a user gesture.
   */
  static get eventSelect() {
    return `${prefix}-tabs-selected`;
  }
<!-- CvTabs.vue -->
<template>
  <cds-tabs
    class="cv-tabs"
    v-bind="props"
    @cds-tabs-selected="e => emits('cds-tabs-selected', e.detail.item.value)"
  >
    <slot></slot>
  </cds-tabs>
</template>

<script setup lang="ts">
import '@carbon/web-components/es/components/tabs/tabs';
import type { CvTabsProps } from './CvTabs.ts';
const props = defineProps<CvTabsProps>();

const emits = defineEmits<{
  // The custom event fired before a tab is selected upon a user gesture. Cancellation of this event stops changing the user-initiated selection.
  (e: 'cds-tabs-beingselected', value: undefined): void;
  // The custom event fired after a a tab is selected upon a user gesture.
  (e: 'cds-tabs-selected', value: undefined): void;
  // The custom event fired before a content switcher item is selected upon a user gesture. Cancellation of this event stops changing the user-initiated selection.
  (e: 'cds-content-switcher-beingselected', value: undefined): void;
  // The custom event fired after a a content switcher item is selected upon a user gesture.
  (e: 'cds-content-switcher-selected', value: undefined): void;
}>();
</script>

v-model

With the previous example we can only define / use v-model by-hand.

<template>
  <cds-tabs class="cv-tabs" v-bind="props" @cds-tabs-selected="onSelect">
    <slot></slot>
  </cds-tabs>
</template>

<script setup lang="ts">
import '@carbon/web-components/es/components/tabs/tabs';
import type { CvTabsProps } from './CvTabs.ts';
const props = defineProps<CvTabsProps>();

const model = defineModel('value');

const onSelect = e => {
  emits('cds-tabs-selected', e.detail.item.value);
  model.value = e.detail.item.value;
};

const emits = defineEmits<{
  // The custom event fired before a tab is selected upon a user gesture. Cancellation of this event stops changing the user-initiated selection.
  (e: 'cds-tabs-beingselected', value: undefined): void;
  // The custom event fired after a a tab is selected upon a user gesture.
  (e: 'cds-tabs-selected', value: undefined): void;
  // The custom event fired before a content switcher item is selected upon a user gesture. Cancellation of this event stops changing the user-initiated selection.
  (e: 'cds-content-switcher-beingselected', value: undefined): void;
  // The custom event fired after a a content switcher item is selected upon a user gesture.
  (e: 'cds-content-switcher-selected', value: undefined): void;
}>();
</script>

props

I think it depends on the custom elements not the vue component it self.

@benceszenassy
Copy link
Contributor Author

@davidnixon let me know if i can help with anything else on this topic

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

No branches or pull requests

2 participants