heybran / vite-babel-proposal-decorators

2 stars 0 forks source link

Related discussions #1

Open vollowx opened 1 year ago

vollowx commented 1 year ago

不经过简化的自定义元素属性看起来是这样的

  // ...
  /**
   * @param {boolean} value
   */
  set checked(value) {
    this.toggleAttribute('checked', value);
  }
  get checked() {
    return this.hasAttribute('checked');
  }
  // ...

想通过装饰器达成这样的效果

  @property(Boolean) checked = false;

但是不知道怎么在装饰器里访问实例化的元素,这是失败的方式

/**
 * @param {BooleanConstructor|StringConstructor} type
 */
export function property(type) {
  /**
   * @param {Object} target
   * @param {string} name
   */
  return (target, name) => {
    if (type === Boolean)
      Object.defineProperty(target, name, {
        get() {
          return target.hasAttribute(name);
        },
        set(flag) {
          target.toggleAttribute(name, Boolean(flag));
        },
      });
    else if (type === String)
      Object.defineProperty(target, name, {
        get() {
          return target.getAttribute(name);
        },
        /**
         * @param {string} val
         */
        set(val) {
          target.setAttribute(name, val);
        },
      });
  };
}

求指教 :thinking:

heybran commented 1 year ago

https://github.com/lit/lit/blob/main/packages/reactive-element/src/decorators/property.ts 对着lit的这个研究了很久,不过目前还是没有找到解决方案。

vollowx commented 1 year ago

一样,一直想原生实现,但是lit的实现依赖很复杂,一直没思路 :joy_cat:

heybran commented 1 year ago

不过是个很有趣的问题,可以继续研究。

heybran commented 1 year ago

其实是我太不专业了,需要更新.babelrc为以下:

{
  "presets": ["@babel/preset-env"],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "version": "2023-05" }]
  ]
}

继续测试...

vollowx commented 1 year ago

我没看完你给的那个仓库的内容,加上版本会有什么区别呢

heybran commented 1 year ago

API是有改变的,现在https://github.com/tc39/proposal-decorators#class-fields 这里kind和name会有数据。

vollowx commented 1 year ago

奥……关键点应该是在如何获取元素上,我试试返回值有没有变化.看这里应该是要用到constructor来绑定setter和getter

heybran commented 1 year ago

OK, 这快你肯定比我有经验多了,静等你分享。

vollowx commented 1 year ago

测试用的代码:

/**
 * @param {{ type: BooleanConstructor|StringConstructor }} options
 */
export function property(options) {
  console.log('1: ', options, this);
  /**
   * @param {Object} target
   * @param {ClassFieldDecoratorContext<Object, boolean> & { name: "checked"; private: false; static: false; }} context
   */
  return function (target, context) {
    console.log('2: ', target, context, this);
  };
}

结果:这次连 prototype 都没了,不返回和元素相关的任何东西 :joy_cat:

EDIT: 我在考虑会不会是 Typescript 的 decorator 和 Babel 的不一样(还没看文档

heybran commented 1 year ago

应该是这样:

export function property(value, { kind, name }) {
  console.log(value);
  console.log({ kind, name });

  if (kind === 'field') {
    return function (initialValue) {
      console.log(this); // 指当前web components
      console.log(`initializing ${name} with value ${initialValue}`);
      /**
       * 这里报错:Uncaught TypeError: Cannot redefine property: checked
       * 这个能解决掉,感觉就接近了?
       */
      Object.defineProperty(this, name, {
        get() {
          return this.hasAttribute(name);
        },
        set(flag) {
          this.toggleAttribute(name, Boolean(flag));
        },
      });
      /**
       * 这里return出来的数值才是checked的初始值
       * 我试了return 1,$0.checked = 1;
       */
      return initialValue;
    };
  }
}

Edit: 参考的提案中的这段代码:https://github.com/tc39/proposal-decorators#class-fields

function logged(value, { kind, name }) {
  if (kind === "field") {
    return function (initialValue) {
      console.log(`initializing ${name} with value ${initialValue}`);
      return initialValue;
    };
  }

  // ...
}

class C {
  @logged x = 1;
}

new C();
// initializing x with value 1
vollowx commented 1 year ago

才发现我的装饰器格式有大问题,之前一直以为是我那么写的 :joy:

不过你这样已经成功了啊(抛开报错不谈)

https://github.com/heybran/vite-babel-proposal-decorators/assets/73375859/626b501c-20e0-4a1e-b8ed-3a2257710648

heybran commented 1 year ago

啊,看到错误我以为是不行,关电脑了,明天瞧瞧,你这个组件库很不错啊!

vollowx commented 1 year ago

其实这组件库就因为我纠结这个装饰器重构和一起好多次了(tiny-materialmix-components,甚至更早之前就在尝试material-web-entity),不过这个错误确实会导致点问题,元素的 connectedCallback 不会正常触发了。我也休息去了,明天再看。

heybran commented 1 year ago

有了一点眉目了,测试代码,目前是不会报错,使用也是没问题了,不过就是defer这里感觉怪怪的。

import { customElement, property } from "./decorators.js";

@customElement('my-counter')
export default class MyCounter extends HTMLElement {
  @property(Boolean) checked = false;
  @property(String) theme;
  // ...
}
const defer = (window.requestIdleCallback || requestAnimationFrame).bind(window);

/**
 * @param {BooleanConstructor|StringConstructor} type
 */
export function property(type) {
  /**
   * @param {undefined} value
   * @param {{ kind: string, name: string | symbol }} options
   */
  return function(value, { kind, name }) {
    if (kind === 'field') {
      return function (initialValue) {
        console.log(`initializing ${name} with value ${initialValue}`);
        /**
         * Undefined at this time
         * Decorators are called (as functions) during class definition, 
         * after the methods have been evaluated but before the constructor 
         * and prototype have been put together.
         * https://github.com/tc39/proposal-decorators#detailed-design
         */
        console.log('descriptor', Object.getOwnPropertyDescriptor(this, name));
        /**
         * Uncaught TypeError: Cannot redefine property: checked
         * 所以需要等待这个class的定义工作结束后再来defineProperty,
         * 把defer换成settimeout也是可以的,但总感觉两个方法都不是最佳的...
         */
        defer(() => {
          Object.defineProperty(this, name, {
            get() {
              if (type === Boolean) {
                return this.hasAttribute(name);
              } else if (type === String) {
                /**
                 * 如果类型是string的话,当组件上面没有添加该属性时,e.g.: <my-button></my-button>
                 * 假设想要关注的属性是theme,此时this.getAttribute('theme')等于null,typeof null是object,
                 * 但是我们想要的类型是string,所以需要加上一个??在这里?
                 */
                return this.getAttribute(name) ?? '';

                /**
                 * 也想到了这个,但是这个不合理,因为写组件时并不知道组件添加的theme值,
                 * 所以应该是不能设置初始值的
                 */
                return this.getAttribute(name) ?? initialValue ?? '';
              }
            },
            set(flag) {
              if (type === Boolean) {
                this.toggleAttribute(name, Boolean(flag));
              } else if (type === String) {
                this.setAttribute(name, flag);
              }
            },
            configurable: true,
            enumerable: true,
          });
        });

        return initialValue;
      };
    }
  }
}
vollowx commented 1 year ago

厉害,我一直没想到用延时解决。现在还有一个想法,就是动态修改元素 observedAttributes(Lit不需要单独定义),类似于这样,但是没想到怎么定义静态属性

/**
 * @param {BooleanConstructor|StringConstructor} type
 */
export function property(type) {
  /**
   * @param {undefined} _value
   * @param {{ kind: string, name: string | symbol }} options
   */
  return function (_value, { kind, name }) {
    if (kind === 'field') {
      /**
       * @param {any} initialValue
       */
      return function (initialValue) {
        defer(() => {
          this.constructor.observedAttributes !== undefined
            ? this.constructor.observedAttributes.includes(name)
              ? null
              : this.constructor.observedAttributes.push(name)
            : (this.constructor.observedAttributes = [name]);
          console.log(this.constructor.observedAttributes)

          console.log(
            `initializing ${String(name)} with value ${initialValue}`
          );

          Object.defineProperty(this, name, {
            get() {
              if (type === Boolean) {
                return this.hasAttribute(name);
              } else if (type === String) {
                return this.getAttribute(name) ?? '';
              }
            },
            set(flag) {
              if (type === Boolean) {
                this.toggleAttribute(name, Boolean(flag));
              } else if (type === String) {
                this.setAttribute(name, flag);
              }
            },
            configurable: true,
            enumerable: true,
          });
        });

        return initialValue;
      };
    }
  };
}
heybran commented 1 year ago

不过还从来没用过lit,回头尝试简单学习下看看是啥样的,你在哪个项目已经在使用decorators了?

heybran commented 1 year ago

https://github.com/vollowx/tiny-material 这个项目怎么暂停了?

vollowx commented 1 year ago

主要原因是由于结构问题复用率低,包括这个 issue 解决的属性同步问题,不用装饰器的话,你可以参考一下 tiny-material 的 InputElement 的实现,还尝试用这种函数来同步属性,依然是代码量大,可读性差,后续也不好维护

当时放弃是在写焦点陷阱的时候,一直没有什么好的方法保证灵活性,这个提交后面就力不从心了

不过这不还在用新知识写 vollowx/m3-web-components 嘛,现在用 .css?inline 换掉了 CSS in JS,也基本放弃了用原生元素,直接自己实现的(input 除外),比以前好很多

heybran commented 1 year ago

还是觉得全部使用原生写的组件好,https://github.com/thepassle/generic-components 这个和你的应该也是差不多,都没有用lit。

vollowx commented 1 year ago

嗯,这个也是放弃了原生元素,完全自定义元素的最大优势之一就是可以高度自定义样式,也不用在自定义元素和原生元素之前同步属性。如果是想用 Lit 实现的使用原生元素组件库,可以看看谷歌的 material-web,大多数组件已经在 Beta 阶段了

vollowx commented 1 year ago
/**
 * @param {string} selector
 */
export function query(selector) {
  /**
   * @param {undefined} _value
   * @param {{ kind: string, name: string }} options
   */
  return function (_value, { kind, name }) {
    if (kind === 'field') {
      /**
       * @param {any} _initialValue
       */
      return function (_initialValue) {
        Object.defineProperty(this, name, {
          get() {
            return this.renderRoot.querySelector(selector);
          },
          configurable: true,
          enumerable: true,
        });
        return this.renderRoot.querySelector(selector);
      };
    }
  };
}

这种初始值和 getter 一样的如何简化呢

vollowx commented 1 year ago

又加了一个函数来解决这个问题

/**
 * @param {(name: string, target: HTMLElement) => PropertyDescriptor} descriptor
 */
function decorateProperty(descriptor) {
  /**
   * @param {undefined} _
   * {{ kind: string, name: string }} options
   */
  return function (_, { kind, name }) {
    if (kind !== 'field') return;

    /**
     * @param {any} _
     */
    return function (_) {
      const _descriptor = descriptor(name, this);
      Object.defineProperty(this, name, _descriptor);
      return _descriptor.get?.();
    };
  };
}

简化为

/**
 * @param {string} selector
 * @todo Add ability to cache
 */
export function query(selector) {
  return decorateProperty((_, target) => {
    return {
      get() {
        return target.renderRoot.querySelector(selector);
      },
      configurable: true,
      enumerable: true,
    };
  });
}
heybran commented 1 year ago

有趣,代码更新到你的仓库里面了吗?

vollowx commented 1 year ago

https://github.com/vollowx/m3-web-components/commit/f26f69110eb75bb8a00cb698930bfaf96075f998

heybran commented 1 year ago

跟你的组件代码一比较,感觉我之前写的组件代码都很乱,我要多花时间在你这个仓库中好好学习了。不过最近忙着找工作,心不静了...

vollowx commented 1 year ago

互相学习吧,看你的项目之前我还不知道有 import css from './file.css?inline' 的用法呢,祝你尽快找到工作 :smile_cat:

heybran commented 1 year ago

谢谢!

vollowx commented 1 year ago

有个不相关的问题,希望有时间能帮忙看看,问题描述在行内

heybran commented 1 year ago

这篇博客好像是谈到这个问题:https://heybran.cn/blog/an-interesting-fact-on-remove-event-listener/

vollowx commented 1 year ago

最近想到 form 元素的问题,又开始犹豫要不要用原生元素代替重写了……

现在的方法:

<md-button
  role="button"
  aria-disabled="false">
  Button
</md-button>

但是 不能和 form 元素联动,更别提 validate 之类的 form 功能

也有个解决方案, 但是 很麻烦

<md-button
  type="submit"
  disabled
  label="Disabled Button"
  data-aria-label="this's a disabled button">
    #shadow-root
      <button
        type="submit"
        disabled aria-label="this's a disabled button">
        Disabled Button
      </button>
</md-button>

要同步属性,重渲染相对不好搞,还要在有含义的属性前面加 data,如 rolearia-*

但是 W3C 的示例里面也提供了很多使用第一种方法的……很纠结

heybran commented 1 year ago

看看shoelace button组件是怎么用的?

vollowx commented 1 year ago

有点离谱…太多要同步的东西了,mixed-components 暂时只打算基本可用

heybran commented 1 year ago

是的,无障碍支持需要花很大的功夫

vollowx commented 1 year ago

不过这应该不在无障碍的范畴内,只是原生 form 的功能难以完全同步,理论上不用原生的 form 也可以实现表单功能,Polymer 官方的组件库就是这个模式,我也是参考的它。

平时一个组件的无障碍功能没做完我都不会 git push 的 :smile:

heybran commented 1 year ago

Shoelace是用的formdata event来使shadowDOM中的元素参与表单提交,试了下挺好用的。

vollowx commented 1 year ago

我有一个想法:customizing native element 实际上允许我们在不做属性同步和其他工作的情况下专注于一些如结构、样式的东西,比如 button ,不过对于使用者来说会增加工作量。

heybran commented 1 year ago

是的,不过目前市面上的组件库好像都没有走这个路,之前在Mastadon和web components discord上面看那些人聊,好像safari这块有点不兼容。

vollowx commented 1 year ago

有,但是确实很少,因为用起来(看起来)很怪,况且 Safari 开发团队表示过不会添加这种不完善的功能(同感)。我个人更希望能在此基础上直接使用新的 tag,而非用冗长的 is

不过又发现了一个功能:HTMLElement.attachInternalsformAssociated 可以添加 <form> 和 ARIA 属性和方法,包括完全自定义元素。我用这个基础实现了一下表单功能:FormMixin,这是演示。除了不能自动设置如 [type="submit"] 等的事件外,应该是基本可用的。

vollowx commented 1 year ago

还有一个用例不好解决:把按钮当作链接使用,对于完全重写的自定义元素来说不好实现。Polymer 官方组件库的实现方法的无障碍功能依然有问题。

heybran commented 1 year ago

https://shoelace.style/components/button#link-buttons

heybran commented 1 year ago

发现一个color pallet:https://yeun.github.io/open-color/ 准备用到组件当中。

vollowx commented 1 year ago

shoelace.style/components/button#link-buttons

这是对于自定义元素内放置原生元素的解决方案,对于重写自定义元素来说不是很好解决……不过应该可以再定义一个链接元素,当按钮元素检测到父元素是链接元素时取消自己的无障碍 role="button"

heybran commented 1 year ago

都周末写组件吗?还是工作日晚上也写?

heybran commented 1 year ago

测试好像是最难搞的,我现在都是手动点点点...

vollowx commented 1 year ago

都周末写组件吗?还是工作日晚上也写?

还在初三呢,寄宿制学校。

heybran commented 1 year ago

我的天呐,初三,羡慕初三的你代码写的就比我好了!

vollowx commented 1 year ago

也没有啦,只是学的比较早,而且因为有强迫症,养成了还算不错的代码风格。

heybran commented 1 year ago

你有玩Mastadon吗?上面很多大佬交流web无障碍相关知识。

vollowx commented 1 year ago

以前没见过,不过简单看了一下,在 #accessibility 下的推文很少啊,是我的查找方式有问题吗?

heybran commented 1 year ago

那就奇怪了,我倒是经常有刷到

Vollow @.***>于2023年9月23日 周六19:33写道:

以前没见过,不过简单看了一下,在 #accessibility 下的推文很少啊,是我的查找方式有问题吗?

— Reply to this email directly, view it on GitHub https://github.com/heybran/vite-babel-proposal-decorators/issues/1#issuecomment-1732288569, or unsubscribe https://github.com/notifications/unsubscribe-auth/ASBBHAICVQBX36NRQ5EKEVTX33CHTANCNFSM6AAAAAA3DZ7ZAU . You are receiving this because you commented.Message ID: @.***>