Skip to content

[labs] Reactive controller adapters for other frameworks #1682

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

Closed
justinfagnani opened this issue Mar 19, 2021 · 31 comments
Closed

[labs] Reactive controller adapters for other frameworks #1682

justinfagnani opened this issue Mar 19, 2021 · 31 comments

Comments

@justinfagnani
Copy link
Collaborator

justinfagnani commented Mar 19, 2021

We would like to enable a framework-agnostic subset of reactive controller to work across frameworks. To do this we need two things from the frameworks: a way to emulate or map the framework lifecycle to the reactive controller lifecycle, and a native composition API or way to extend the framework component model to make adding controllers possible.

Many frameworks seem to have the basic lifecycle required by reactive controllers: hostConnected, hostDisconnected, hostUpdate, hostUpdated, and host.requestUpdate() - or they can be emulated. Not all have a way of extending the component model though

Lifecycle React hooks Angular Vue Ember Svelte
hostConnected
initial render

ngAfterContentInit

onMounted

init

onMount
hostDisconnected
useLayoutEffect

ngOnDestroy

onUnmounted

didDestroyElement

onDestroy
hostUpdate
hook body

ngOnChanges

onBeforeUpdate

willUpdate

beforeUpdate
hostUpdated
useLayoutEffect

ngAfterContentChecked

onUpdated

didUpdate

afterUpdate
requestUpdate
useState
⚠️
not needed w/ zones?

getCurrentInstance().update()
⚠️ ?
writable store
updateComplete
useLayoutEffect

ngOnChanges w/ Promise

nextTick
⚠️ ? ✅ tick
Composition or
Extendable

custom hook

mixins

composition API
⚠️ ?
Mixin or CoreObject?

lifecycle hooks and stores
PR / Prototype #1532 Prototype Prototype Prototype

✅ = There's a way to emulate
⚠️ ? = Unsure how to implement, but there's probably a way
🆘 = Seems like there isn't a way to emulate

We would like the mapping or controller wrapper to end up being as idiomatic as possible in the host framework. If the framework already has a composition API, like React hooks, we want to wrap reactive controllers into that API. If the framework doesn't have a similar API, then we could try to extend the component model with a subclass or mixin and add a way to declare controllers.

See #1532 for an example of creating a useController() hook for React that emulates and drives the reactive controller lifecycle. After wrapping a reactive controller with useController(), the resulting hook is used like any other hook:

Definition:

import * as React from 'react';
import {useController} from '@lit-labs/react/use-controller.js';
import {MouseController} from '@example/mouse-controller';

export const useMouse = () => {
  const controller = useController(React, (host) => new MouseController(host));
  return controller.position;
};

Idiomatic usage:

import {useMouse} from './use-mouse.js';

const Component = (props) => {
  const mousePosition = useMouse();
  return (
    <pre>
      x: {mousePosition.x}
      y: {mousePosition.y}
    </pre>
  );
};
@justinfagnani
Copy link
Collaborator Author

justinfagnani commented Mar 19, 2021

I had thought that Svelte didn't have a way for an external object to hook the lifecycle or force an update, but @RyanCarniato
helped me out on Twitter. I have a PoC of a Svelte wrapper now: https://svelte.dev/repl/51e7c2c30311490dbf2078a781ed1e2c?version=3.35.0

Svelte usage seems like it could be fairly idiomatic. The controller is modeled as a store and with auto-bindings you can refer to the controller state in the component and template:

import {makeSvelteController} from './controllers.js';
import {MouseController} from './mouse-controller.js';
export const mouseController = makeSvelteController((host) => new MouseController(host))
<script>
 import {mouseController} from './svelte-mouse-controller.js'
 const mouse = mouseController();
</script>
<pre>
x: {$mouse.pos.x}
y: {$mouse.pos.y}
</pre>

@justinfagnani
Copy link
Collaborator Author

Proof-of-concept AngularControllerHost here: https://stackblitz.com/edit/angular-ivy-uuiqpj?file=src%2Fapp%2Fangular-controller-host.ts

@tsavo-vdb-knott
Copy link

Love to see all of this 🎉

Nevertheless, will you be targeting both Renderer2 and/only Ivy lifecycles for Angular?

@userquin
Copy link

userquin commented Mar 21, 2021

For vue 2/3, you can use $forceUpdate component instance method for requestUpdate, but using it, in 99.99% of cases, you’ve made a mistake somewhere:

@justinfagnani
Copy link
Collaborator Author

will you be targeting both Renderer2 and/only Ivy lifecycles for Angular?

I'm not sure. What are those? :)

@userquin
Copy link

For vue you can see vue-lit.
For vue composition api, you can see a collection on vueuse and vue-composable.

@justinfagnani
Copy link
Collaborator Author

@userquin how do you call $forceUpdate from within a setup function? I don't see how to get a reference to the instance.

@userquin
Copy link

Try this:

<script lang="ts">
import { defineComponent, getCurrentInstance } from 'vue'
export default defineComponent({
  setup() {
    const vm = getCurrentInstance()
    const update = () => {
      vm?.ctx?.$forceUpdate()
    }
    return { update }
  },
})
</script>
<template>
  <div>
    ...
  </div>
  <button @click="update">
    Refresh
  </button>
</template>

@userquin
Copy link

or just use vm?.update():

imagen

@userquin
Copy link

userquin commented Mar 21, 2021

or with script setup (similar to svelte but without exports):

<script setup lang="ts">
import { getCurrentInstance } from 'vue'    

const vm = getCurrentInstance()

const update = () => {
  vm?.update()
}
</script>
<template>
  <div>
    ...
  </div>
  <button @click="update">
    Refresh
  </button>
</template>

@userquin
Copy link

@justinfagnani the problem is that with vue 3 I cannot make it to rebuild, the nodes are marked as literals, so there is no need to update the content once on dom.

If we include some reactive state, we don't need to force repaint... just changing the reactive object will be enough...

For example:

<script lang="ts">
import { defineComponent, getCurrentInstance, ref } from 'vue'
export default defineComponent({
  setup() {
    const counter = ref(0)
    const vm = getCurrentInstance()
    const update = () => {
      counter.value++
      //vm?.update()
    }
    return { update, counter }
  },
})
</script>
<template>
  <div>
    <button @click="update">
      Refresh
    </button>
    <div :class="`c-${counter}`">
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
    </div>
  </div>
</template>

With no ctx?.update() , also the children are not rerendered (just open dev tool and see how the class attr is rerendered)... with vue 2 this will be another history.

@userquin
Copy link

to be sure the update is triggering, use this, you will see UPDATED on console:

<script lang="ts">
import { defineComponent, getCurrentInstance/*, ref */, onUpdated } from 'vue'
export default defineComponent({
  setup() {
    // const counter = ref(0)
    const vm = getCurrentInstance()
    const update = () => {
      // counter.value++
      vm?.update()
    }
    onUpdated(() => {
      console.log('UPDATED')
    })
    return { update/*, counter */ }
  },
})
</script>
<template>
  <div>
    <button @click="update">
      Refresh
    </button>
    <!--    <div :class="`c-${counter}`">-->
    <div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
      <div>hey</div>
    </div>
  </div>
</template>

@justinfagnani
Copy link
Collaborator Author

Thanks @userquin - getCurrentInstance() worked. Proof of concept makeVueController() here: https://stackblitz.com/edit/vue-qniewc?file=src%2Fcomponents%2FHelloWorld.vue

@userquin
Copy link

@justinfagnani on vue 3 all hooks have been changed/renamed, see Lifecycle Hooks.

You can split vue column into 2 columns, first one for vue 2 and second for vue 3.

@justinfagnani
Copy link
Collaborator Author

I don't think I'll prototype Vue 2, since Vue 3 is the latest. Someone else could though.

@userquin
Copy link

FYI: hook names on your table are for v2 or when using Options Api in v3. When using setup function (Composition Api) the names on the table cannot be used.

@LayZeeDK
Copy link

Angular would use ngAfterContentInit for the hostConnected hook.

@userquin
Copy link

Click on the Prototype link in the table footer...

@LayZeeDK
Copy link

LayZeeDK commented Mar 21, 2021

About Angular, you were uncertain about why you don't need to do anything for the requestUpdate hook. You could use ChangeDetectionStrategy.OnPush to figure out what you need to do to support optimized change detection as this is a very common use case.

The solution would be something like this.changeDetectorRef.detectChanges() but next we need optimizations such as coalescing and scheduling change detection cycles.

@tsavo-vdb-knott
Copy link

So - Angular is moving from Renderer2 to Ivy and really focusing on dropping a reliance on Zone.js - that being said you will have to choose what you want to support/if you want to support the old Renderer2 at all. Personally I would recommend supporting both as the most likely users of LitElement are those incrementally updating a large dated Application.

The following is a great library to study with regards to Angular Component (Template) rendering & Lifecycles.
Rendering Strategies and Change Detection Scheduling

Additionally in most contemporary cases Ivy + a ChangeDetectionStrategy is used along with ChangeDetectorRef is used to manually call a markForCheck() similar to requestUpdate()

With regards to Renderer2 there are some helpful callbacks to know about the state of the current render cycle, often this is helpful when one is using a (RendererFactory)[https://angular.io/api/core/RendererFactory2] to gain granular control/integration over the process. Helpful Renderer2 DOM Algo Insights

These API's/Processes In conjunction to the current lifecycle should provide enough tools to integrate ReactiveControllers no prob.

LMK if I can help further.

-T

@LayZeeDK
Copy link

Angular is moving from Renderer2 to Ivy

Renderer2 will stick around. It's just not required if only using Angular web platforms.

really focusing on dropping a reliance on Zone.js

Zoneless Angular applications is a long-term effort. With async-await patched in NgZone by Angular, it might not have a very high priority.

@userquin
Copy link

@justinfagnani you can use Vue SFC Playground similar to Svelte REPL using script setup: see the prototype you build for Vue 3 here

@bennypowers
Copy link
Collaborator

bennypowers commented Jun 22, 2021

  • haunted
  • hybrids I've got hopes to move this to a repo for hybrids extras or something. At the moment @smalluban doesn't have the bandwidth to maintain such a project though, so it'll stay here for now
  • FAST. I've had some discussions with FAST team and made a few attempts but haven't grokked it yet. key terms in FAST's model are: $fastController, Behaviour, Binding. @EisenbergEffect was quite helpful in FAST discord, so it's surely PEBCAK
  • Custom Element Mixin perhaps a good candidate for inclusion in labs?
  • Atomico

@KonnorRogers
Copy link

@bennypowers I have a POC for FAST reactive controllers. It makes use of a private API.

element.$fastController.renderTemplate()

https://github.com/ParamagicDev/reactive-fast-element/blob/d0fa0942cef1e5142405248e43a4249f18922466/src/fast-reactive-mixin.ts#L30-L35

I adopted the examples from Lit's example docs and theyall appear to work.

https://github.com/ParamagicDev/reactive-fast-element/tree/main/src/examples

I know its not the official "behavior" way and ive been told it bypasses a lot of FAST's performance enhancements for templates, but as far as I can tell, it works 🤷

Its available both as an extendable base class, or as a mixin.

@claviska
Copy link

claviska commented Feb 9, 2022

I know its not the official "behavior" way and ive been told it bypasses a lot of FAST's performance enhancements for templates

This is true, but long term concern is that the private method you're using will become inaccessible, so if they decide to add a # in front of it, re-rendering will be completely broken with no other way to force it.

It's worth noting that Reactive controllers can do a lot more than re-rendering, so there's still value in having an adapter. It's unfortunate that FAST's position on this is to build a FAST behavior for each RC you want to use (source).

Surely it would be more efficient to have a single adapter — or to build support for RCs into the library. Maybe we can continue to push for $fastController.requestUpdate(), which Rob seemed more open to in this discussion on their Discord.

@bennypowers
Copy link
Collaborator

Is there an open issue for this on FAST?

@claviska
Copy link

claviska commented Feb 9, 2022

Is there an open issue for this on FAST?

Not that I'm aware of. We've only discussed it briefly in the Discord.

@KonnorRogers
Copy link

@bennypowers and @claviska id be happy to open one for official tracking.

@kevinpschaaf kevinpschaaf moved this to 🧊 Icebox in Lit Project Board Feb 10, 2022
@justinfagnani justinfagnani moved this from 🧊 Icebox to 📋 Triaged in Lit Project Board Sep 8, 2022
@bgotink
Copy link
Contributor

bgotink commented Oct 19, 2022

I'm looking to migrate a large angular library to lit, and was searching for an implementation of the ReactiveControllerHost. I've extended the prototype linked in the OP: prototype on stackblitz.

  • Add support for ChangeDetectionStrategy.OnPush by having requestUpdate call ChangeDetectorRef#markForCheck
  • Run controller code outside of the Angular zone, because it's highly unlikely that code not specifically meant for Angular is written with zones in mind.

Especially the latter is very important for performance reasons. Angular runs change detection every time code finishes running in the Angular zone. If the controller runs in the zone, every event listener will automatically run in the Angular zone too, which is disastrous for event listeners like mousemove that run very often.

@justinfagnani
Copy link
Collaborator Author

Closing this for now since we don't really plan on making more controller adapters for other frameworks at the moment. If it comes up again, we know we can for most frameworks.

@github-project-automation github-project-automation bot moved this from 📋 Triaged to ✅ Done in Lit Project Board Oct 2, 2024
@userquin
Copy link

userquin commented Oct 6, 2024

If someone insterested in Vue3, here an SB repo using Vite instead Vue CLI, using both Options and Composition API: check the README.md and the src/components/HelloWorld.vue component:

https://stackblitz.com/edit/vitejs-vite-22tb3u?file=README.md

/cc @justinfagnani current Vue SB playground not working

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

No branches or pull requests

10 participants