Open vollowx opened 1 year ago
https://github.com/lit/lit/blob/main/packages/reactive-element/src/decorators/property.ts 对着lit的这个研究了很久,不过目前还是没有找到解决方案。
一样,一直想原生实现,但是lit的实现依赖很复杂,一直没思路 :joy_cat:
不过是个很有趣的问题,可以继续研究。
其实是我太不专业了,需要更新.babelrc为以下:
{
"presets": ["@babel/preset-env"],
"plugins": [
["@babel/plugin-proposal-decorators", { "version": "2023-05" }]
]
}
继续测试...
我没看完你给的那个仓库的内容,加上版本会有什么区别呢
API是有改变的,现在https://github.com/tc39/proposal-decorators#class-fields 这里kind和name会有数据。
OK, 这快你肯定比我有经验多了,静等你分享。
测试用的代码:
/**
* @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 的不一样(还没看文档
应该是这样:
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
才发现我的装饰器格式有大问题,之前一直以为是我那么写的 :joy:
不过你这样已经成功了啊(抛开报错不谈)
啊,看到错误我以为是不行,关电脑了,明天瞧瞧,你这个组件库很不错啊!
其实这组件库就因为我纠结这个装饰器重构和一起好多次了(tiny-material和mix-components,甚至更早之前就在尝试material-web-entity),不过这个错误确实会导致点问题,元素的 connectedCallback
不会正常触发了。我也休息去了,明天再看。
有了一点眉目了,测试代码,目前是不会报错,使用也是没问题了,不过就是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;
};
}
}
}
厉害,我一直没想到用延时解决。现在还有一个想法,就是动态修改元素 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;
};
}
};
}
不过还从来没用过lit,回头尝试简单学习下看看是啥样的,你在哪个项目已经在使用decorators了?
https://github.com/vollowx/tiny-material 这个项目怎么暂停了?
主要原因是由于结构问题复用率低,包括这个 issue 解决的属性同步问题,不用装饰器的话,你可以参考一下 tiny-material 的 InputElement 的实现,还尝试用这种函数来同步属性,依然是代码量大,可读性差,后续也不好维护
当时放弃是在写焦点陷阱的时候,一直没有什么好的方法保证灵活性,这个提交后面就力不从心了
不过这不还在用新知识写 vollowx/m3-web-components 嘛,现在用 .css?inline
换掉了 CSS in JS,也基本放弃了用原生元素,直接自己实现的(input 除外),比以前好很多
还是觉得全部使用原生写的组件好,https://github.com/thepassle/generic-components 这个和你的应该也是差不多,都没有用lit。
嗯,这个也是放弃了原生元素,完全自定义元素的最大优势之一就是可以高度自定义样式,也不用在自定义元素和原生元素之前同步属性。如果是想用 Lit 实现的使用原生元素组件库,可以看看谷歌的 material-web,大多数组件已经在 Beta 阶段了
/**
* @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 一样的如何简化呢
又加了一个函数来解决这个问题
/**
* @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,
};
});
}
有趣,代码更新到你的仓库里面了吗?
跟你的组件代码一比较,感觉我之前写的组件代码都很乱,我要多花时间在你这个仓库中好好学习了。不过最近忙着找工作,心不静了...
互相学习吧,看你的项目之前我还不知道有 import css from './file.css?inline'
的用法呢,祝你尽快找到工作 :smile_cat:
谢谢!
最近想到 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
,如 role
, aria-*
。
但是 W3C 的示例里面也提供了很多使用第一种方法的……很纠结
看看shoelace button组件是怎么用的?
有点离谱…太多要同步的东西了,mixed-components 暂时只打算基本可用
是的,无障碍支持需要花很大的功夫
不过这应该不在无障碍的范畴内,只是原生 form 的功能难以完全同步,理论上不用原生的 form 也可以实现表单功能,Polymer 官方的组件库就是这个模式,我也是参考的它。
平时一个组件的无障碍功能没做完我都不会 git push
的 :smile:
Shoelace是用的formdata event来使shadowDOM中的元素参与表单提交,试了下挺好用的。
我有一个想法:customizing native element 实际上允许我们在不做属性同步和其他工作的情况下专注于一些如结构、样式的东西,比如 button
,不过对于使用者来说会增加工作量。
是的,不过目前市面上的组件库好像都没有走这个路,之前在Mastadon和web components discord上面看那些人聊,好像safari这块有点不兼容。
发现一个color pallet:https://yeun.github.io/open-color/ 准备用到组件当中。
这是对于自定义元素内放置原生元素的解决方案,对于重写自定义元素来说不是很好解决……不过应该可以再定义一个链接元素,当按钮元素检测到父元素是链接元素时取消自己的无障碍 role="button"
。
都周末写组件吗?还是工作日晚上也写?
测试好像是最难搞的,我现在都是手动点点点...
都周末写组件吗?还是工作日晚上也写?
还在初三呢,寄宿制学校。
我的天呐,初三,羡慕初三的你代码写的就比我好了!
也没有啦,只是学的比较早,而且因为有强迫症,养成了还算不错的代码风格。
你有玩Mastadon吗?上面很多大佬交流web无障碍相关知识。
以前没见过,不过简单看了一下,在 #accessibility
下的推文很少啊,是我的查找方式有问题吗?
那就奇怪了,我倒是经常有刷到
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: @.***>
不经过简化的自定义元素属性看起来是这样的
想通过装饰器达成这样的效果
但是不知道怎么在装饰器里访问实例化的元素,这是失败的方式
求指教 :thinking: