-
Notifications
You must be signed in to change notification settings - Fork 75
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
Web Components: Discussion about how to use Crank.js for custom elements #47
Comments
IMHO it makes sense to provide (if somehow possible) meta information about the supported properties, attributes and events and also about the type of the properties. This makes for example auto-converting of (string) attributes to properties and vice versa much easier. In case that it is not really clear what I mean please find here simple demos where these features are used (FYI: These demos use a completely unimportant "just for fun" pre-alpha toy library, these demos are only meant as an example for a function based web component library with the above mentioned features): [Edit: Updated demos] |
Copy-pasting what I wrote in a reddit discussion https://www.reddit.com/r/javascript/comments/g1zj87/crankjs_an_alternative_to_reactjs_with_built_in/fnjwa5o?utm_source=share&utm_medium=web2x:
I really like the idea of a web component interface, and think it could be really important for solving the problem that React’s
The one thing is that we can’t really iterate over Lots of room to explore. I think this is a really important feature and I’m curious to hear what people’s thoughts are. |
[Important: We should concentrate first on enhancing the general Crank design patterns ... "web components" should really not have a high priority at the moment... 😉] I am very strongly of the opinion that for inspiration it's always good to see code examples of how others are trying to solve the problems you are currently trying to solve (independent whether you are a big fan of those solutions or not). So please allow me to show you another example using this web component toy library I have already mentiond above. component('simple-counter', {
props: {
label: prop.str.opt('Counter'),
initialCount: prop.num.opt(0)
},
methods: ['reset']
}, (c, props) => {
const
[state, setState] = useState(c, { count: props.initialCount }),
onIncrement = () => setState('count', it => it + 1)
useMethods(c, {
reset(count = 0) {
setState({ count })
}
})
return () => html`
<button @click=${onIncrement}>{props.label} {state.count}</button>
`
}) Please find a running demo here: |
Again, for inspiration, here's a litte demo of a custom element that uses it's own (dedicated) CSS styles and has two different slots. Again, the goal is to find a better and more Crankish solution than this. Please find here an example implementation using this web component toy library I have already used for the examples above. My previous demos used BTW: That toy library is still buggy as hell :-(... don't expect that anything else is working beyond these little demos: https://codesandbox.io/s/js-elements-demo-uxkfu?file=/src/index.js |
@mcjazzyfunky Hmmm definitely not a cranky API but interesting and impressive. An interesting conversation on web components: https://twitter.com/RyanCarniato/status/1257806356947464193 |
The fact, that the stateful components in my demos are based on a complete different pattern than Crank is not important here (just replace the function argument with a crank component function and you have a more Cranky API). What I've tried to show is that Brian's example above
could be extended to something like: const MyComponent = registerCrankComponent(tagName, options, crankyFunc) where the argument // [Edit]
// Or as an alternative maybe something like this,
// in case those web components are completely based
// on "Crank.js the library" and not only "Crank the design pattern":
const MyComponent = component(config, crankyFunc)
CrankWebComponent.register(tagName, MyComponent) [Edit] A bit later, I doubt that this "alternative" is really working the way I wanted it to work. I think |
Hmmmh, I think I've changed my mind a bit here.
After shortening the function name and adding an argument to provide component meta information (which and what props does the component have, which props are optional, which are required, and what are the default prop values?) you get:
In the suggestions above My following proposal is different: Why not just ALWAYS use a pure, normal function for that third argument (nothing async and no generators). Just implement the whole complex component that shall be used as web component completely as a usual crank component function and then just wrap it directly as a web component, where I think a pure function will completely do the job. Please find here a demo that hopefully shows what I mean: » Demo (in the demo I use a slightly different form - which I personally like better: |
@mcjazzyfunky Interesting! I like the prop/attr normalization system you got going. The big thing web components need is an imperative API; that’s what motivates their usage above and beyond just regular Crank components. For instance, if I do |
Like said, a second argument for that A good thing about this PS: The demos do not show how to handle events but it will be just: defineCrankElement('crank-counter', {
props: {
...,
onSomeEvent: prop.func.opt()
},
... ... not necessarily very easy to implement, but doable. |
@mcjazzyfunky Wow that’s getting there! One thought I have. The benefit of inheritance is that you can just define methods directly on the class, rather than as a callback inside the component, which feels a little mind-bending. It means that your Crank components have to be aware of your webcomponent logic, which feels off to me. I do like the idea that the web component class only takes a single pure function. That simplifies a lot of things, and I’m not sure why I wanted the web component API to use generator functions in the first place. What about something like this? class MyComponent extends McJazzyFunkyComponent {
constructor() {
super({count: prop.num.req(), label: prop.str.opt('Count')}, (props) => (
<Counter count={props.count} label={props.label} />
));
}
reset() {
this.count = 0; // triggers internal connectedCallback logic
}
} You’re free to use whatever API you want of course, just brainstorming some API ideas. |
Actually that was the idea (maybe not the best idea 😄): Write a common Crank component that has all properties and imperative methods that you want and then with a few lines of code wrap that Crank component 1:1 in a custom element class (even if you do not see the class in my demo - under the hood there is a custom element class of course). After that you have a Crank component and a custom element component that have both equal features. In your latest examples you made the Anyway, as that topic is not really very urgent, I think it makes sense to wait for other API proposals and ideas and reevaluate again in some weeks. |
Okay, maybe my last proposal will not fit all needs. https://codesandbox.io/s/crank-webcomponent-demo-forked-uxi0m?file=/src/index.js defineCrankElement('crank-counter', {
props: {
initialCount: prop.num.opt(0),
label: prop.str.opt('Counter')
},
methods: ['reset'],
*main(props, ctx, setMethods) {
let count = props.initialCount
const onIncrement = () => {
++count
ctx.refresh()
}
setMethods({
reset: (n = 0) => {
count = n
ctx.refresh()
}
})
for (props of ctx) {
yield (
<button onclick={onIncrement}>
{props.label}: {count}
</button>
)
}
}
}) I personally prefer this function based syntax. // Abstract class CrankComponent does NOT extend anything
// (especially not HTMLElement).
// CrankComponent implements the Crank context interface.
@component('crank-counter') // will register custom element
class Counter extends CrankComponent {
@prop(Number)
initialCount = 0
@prop(String)
label = 'Counter'
@state() // with auto-refresh support
count = 0
@method()
reset(n: number = 0) {
this.count = n
}
*main() {
this.count = this.initialCount
const onIncrement = () => (++this.count)
while (true) {
yield (
<button onclick={onIncrement}>
{this.label}: {this.count}
</button>
)
}
}
} [Edit -hours later] Hmm, maybe I prefer this syntax to the one that I have implemented in the demo above (shortening https://codesandbox.io/s/crank-webcomponent-demo-forked-ydsbt?file=/src/index.js const counterMeta = {
name: 'crank-counter',
props: {
initialCount: prop.num.opt(0),
label: prop.str.opt('Counter')
},
methods: ['reset']
}
defineElement(counterMeta, function* (props, setMethods) {
let count = props.initialCount
const onIncrement = () => {
++count
this.refresh()
}
setMethods({
reset: (n = 0) => {
count = n
this.refresh()
}
})
for (props of this) {
yield (
<button onclick={onIncrement}>
{props.label}: {count}
</button>
)
}
}) |
Wouldn't it be great to implement custom elements in a "crankish" 😄 way?
This is a discussion thread to gather all ideas to be found about the question how to use Crank.js or Crank.js patterns to implement custom elements.
This is a brainstorming, nobody expects a fully sophisticated proposal. So please share every idea that comes to your mind: Requirements, API suggestions, best practices , dos and don'ts, known pitfalls etc.
Here's a list of some popular web component libraries for inspiration:
The text was updated successfully, but these errors were encountered: