Giter Club home page Giter Club logo

Comments (12)

brainkim avatar brainkim commented on May 14, 2024 3

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 actually think you can add public methods to function components in React using the ā€œuseImperativeHandleā€ hook, but I think the API is kinda hinky. I agree with you 100%, one good metric for a framework is if its components can be exported and embedded in other frameworks, and I think web components play a key role in providing a uniform, imperative interface. I was gonna provide a way to create web components with Crank but didnā€™t get the chance to figure out the API yet.

I sketched out what I wanted in my head: I want the whole props/attr stuff to be normalized, I want to reuse the generator pattern that Crank does for stateful components, and I want declarative JSX, not templates. But because you need to respond to each prop/attr individually, the API is gonna have to be a little different. I thought maybe something like this:

CrankWebComponent.register("my-video", function *(instance) {
  instance.play = () => {
    this.playing = true;
  }

  for (const [name, value] of this) {
    // some code which responds to each new property
    yield (
      <div>
        <video />
      </div>
    );
  }
});

As you can see, not fully fleshed out, but the idea is that you would just provide a generator function and Crank would create the WebComponent class for you and normalize the props/attr changes?

I dunno, I think web components get a bad rap, especially cuz itā€™s 4 separate technologies and most people havenā€™t even tried using them, and Iā€™m really excited to try experimenting with them.

I really like the idea of a web component interface, and think it could be really important for solving the problem that Reactā€™s useImperativeHandle/class refs solves. I think whatever web component library we create should be:

  1. generator-based
  2. synchronous
  3. JSX not templates

The one thing is that we canā€™t really iterate over this and get props, because web components are mutated using individual props, and you have to respond to each prop update and set other props based on each individual prop update. It would be nice to have a conventional way to deal with attr/props mismatches too.

Lots of room to explore. I think this is a really important feature and Iā€™m curious to hear what peopleā€™s thoughts are.

from crank.

mcjazzyfunky avatar mcjazzyfunky commented on May 14, 2024 2

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.
Moreover, explicit default property value declaration may help that reading element properties or attributes (like someElement.something / someElement.getAttribute('something')) will work out of the box (unfortunately, when using declarative, explicit default prop values then there's a good change that you'll get some subtle problems in TS typing - at least with the current TS version ... this will hopefully change in future).

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]
https://codesandbox.io/s/tender-water-g56mc?file=/src/index.js
https://codesandbox.io/s/nameless-brook-hc5bf?file=/src/index.js
https://codesandbox.io/s/gracious-tereshkova-ftlyx?file=/src/index.js
https://codesandbox.io/s/wispy-darkness-tex5c?file=/src/index.js

from crank.

mcjazzyfunky avatar mcjazzyfunky commented on May 14, 2024 1

[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.
Hope you say to yourself "Mmmh, okay ... I see ... but I can do better ... hold my beer ..." ;-) and try to find better and in particular more Crankish solutions ...
This demo shows one possible answer to React's useImperativeHandle (be aware that that little c thingy is basically something like Cranks this/Context, also please be aware that everything here works completely (!) different than with React - don't be confused). [Off-topic: In near future I will - again for inspiration - show a similar little demo that will show a way to handle slots (aka. children 'n stuff) and also CSS with custom element's that use shadow DOM (it is a bit more challenging than it might sound) ... to be continued :-)]

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:
https://codesandbox.io/s/dazzling-spence-s1zme?file=/src/index.js

from crank.

mcjazzyfunky avatar mcjazzyfunky commented on May 14, 2024 1

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

CrankWebComponent.register(tagName, crankyFunc)

  1. generator-based
  2. synchronous
  3. JSX not templates

could be extended to something like:

const MyComponent = registerCrankComponent(tagName, options, crankyFunc)

where the argument options allows to specify some meta data like the involved props, possible default props, prop types, method names, slot names etc.
If the register/registerCrankComponent function will return something (function or whatever - not a string) that can be used as first argument of the createElement function then the whole thing could be properly typeable in TS (<my-component ...> will normally not be type safe, but <MyComponent ...> will).

// [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 MyComponent must know the tagName so something like the first suggestion might be better.

from crank.

mcjazzyfunky avatar mcjazzyfunky commented on May 14, 2024 1

Hmmmh, I think I've changed my mind a bit here.
Above API suggestions were (more or less):

CrankWebComponent.register(tagName, crankyFunction)

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:

defineCrankElement(tagName, meta, crankyFunction)

In the suggestions above crankyFunction was always meant to be either a "normal" function or an async function or a sync generator function or an asnc generator function.

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: defineCrankElement(tagName, { props?, slots?, render }). Custom components often need imperative methods, a topic which is not handled in the demo. I think a single second argument ref or setMethods for that render method should work.
[Edit] Updated demo to also show this setMethods functionality.

from crank.

mcjazzyfunky avatar mcjazzyfunky commented on May 14, 2024 1

The big thing web components need is an imperative API

Like said, a second argument for that render method (which could be called ref or setMethods or whatever name is preferred) will do the job.
I've updated my demo above to show this setMethods stuff (basically the couterpart to React's useImperativeHandle) and some imperative component prop updating:
https://codesandbox.io/s/crank-webcomponent-demo-forked-0d0km?file=/src/index.js

A good thing about this defineCrankElement approach is that if you use a simple adapter pattern, then the
implementation of definePreactElement is only about 5 additional lines of code away.
Same for a defineDyoElement etc. (only a defineReactElement will be more complicated, as React is not very web component friendly)

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.

from crank.

mcjazzyfunky avatar mcjazzyfunky commented on May 14, 2024 1

It means that your Crank components have to be aware of your webcomponent logic

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.
If that does not make sense for let's say more sophisticated components, then I think the whole idea itself may not be really helpful.

In your latest examples you made the Counter component stateless and instead the web component stateful.
But then: Why does the the custom element as both a writable count property plus a reset method?

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.

from crank.

mcjazzyfunky avatar mcjazzyfunky commented on May 14, 2024 1

Okay, maybe my last proposal will not fit all needs.
Please find here a modification of the demo where the configuration parameter main is basically a common Crank function (all four function types supported). The only difference is that the crank context will be passed as second argument and also there is a third argument setMethods (using this here would feel a bit odd IMHO, but that's just a not-so-important detail, I guess).

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.
But I guess most folks will prefer a class-based solution (I think this is more or less a matter of taste).
Unfortunately it will take some time till this decorator and field declaration features will be available in the ECMAScript standard.

// 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 defineCrankElement to defineElement and using this again).
Maybe that looks a bit more crank-esque.

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>
    )
  }
})

from crank.

mcjazzyfunky avatar mcjazzyfunky commented on May 14, 2024

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.
For those who are not yet familiar with web components: Custom elements can (but they do not have to!) use a so called "Shadow DOM" which isolated the CSS classes of the document from the CSS classes dedicated for the custom element. If you want to use CSS classes inside of the custom element's "Shadow DOM" you have to add the corresponding style element to the component's shadow DOM itself (for example if the document uses Bootstrap then "Shadow DOM" custom elements cannot use those Bootstrap CSS classes of the document - you'll have to load the Bootstrap styles also inside of the custom element to use them).
Also it's important to know that if your custom element uses slots it always has to use "Shadow DOM".

Please find here an example implementation using this web component toy library I have already used for the examples above.
The important part is the implementation of component InfoBox aka <info-box ...> especially all occurrences of the word "slot".

My previous demos used lit-html, which is a great library, but unfortunately I personally prefer to implement in TypeScript and lit-html does not allow the same level of type safety as you are used with React and TSX. So this time I've tried to show a way how it's possible (in theory) to be fully typesafe using JSX (by using <InfoBox...> instead of <info-box> ... and as a little gimmick that InfoBox function does out-of-the-box also allow a non-JSX way to build virtual DOM trees (in case you want to implement in pure ECMAScript) => see "demo-2".

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

from crank.

brainkim avatar brainkim commented on May 14, 2024

@mcjazzyfunky Hmmm definitely not a cranky API but interesting and impressive.

An interesting conversation on web components: https://twitter.com/RyanCarniato/status/1257806356947464193

from crank.

brainkim avatar brainkim commented on May 14, 2024

@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 document.getElementsByTagName("crank-counter")[0] in your example, the element should have custom methods or properties which allow me to affect rendering. Like in the above example it would make sense for there to be an imperative reset method, which resets the counter to the initial value. And you should probably also be able to get/set the label of the counter.

from crank.

brainkim avatar brainkim commented on May 14, 2024

@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.

from crank.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    šŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. šŸ“ŠšŸ“ˆšŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ā¤ļø Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.