Skip to content
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

[labs/context] Request to allow custom ContextProvider in @provide decorator #3692

Closed
1 task done
joezappie opened this issue Feb 23, 2023 · 4 comments
Closed
1 task done

Comments

@joezappie
Copy link

joezappie commented Feb 23, 2023

Should this be an RFC?

  • This is not a substantial change

Which package is this a feature request for?

Context (@lit-labs/context)

Description

I've been having a discussion in #3685 about how to deal with using an instance of a class as the value for the context value. Issue is, I want to encapsulate the logic for something, such as a timer that keeps track of the timer and status (stop, running, paused) without having to make the providing element know anything about it - aka it should be able to update its internal values itself.

@vdegenne informed me I could do this with a reactive controller. Since ContextProvider is already a reactive controller, I modified his example to extend ContextProvider, cutting out a bunch of code and allowing the timer to call setValue directly and without having to add another function to the providing element. Works pretty well:

Playground Demo

Proposal

Issue is I cannot use @provide() directly as that forces creation of a ContextProvider. What I'd like to see is the ability to optionally pass in a provider class to use for that. I think this unlocks a lot of deeper possibilities with context providers if you can make your own custom provider class.

@provide({ context: timerContext, provider: TimerProvider});

Alternatives and Workarounds

Don't use the decorator and manually call it but that's no fun:

public timerContext = new TimerProvider(this, timerContext); 
@joezappie joezappie changed the title Allow custom ContextProvider to be used in @lit-labs/context [labs/context] Request to allow custom ContextProvider in @provide decorator Feb 23, 2023
@rictic
Copy link
Collaborator

rictic commented Jul 6, 2023

The @provide decorator is intended for providing values set into the decorated field. Your example syntax isn't legal JS or TS because there isn't a decorated member:

@provide({ context: timerContext, provider: TimerProvider});

@rictic rictic closed this as completed Jul 6, 2023
@joezappie
Copy link
Author

@rictic Could this be relooked at? I'm back to wishing something like this was built into Lit.

I apologize for not including the full example and just showing what I thought the @provide syntax should be. I've updated my Lit Playground Example where I've modified the @provide decorator to allow a custom ContextProvider to be used.

Heres a snippet from the playground of the very minor changes I'd like to see to allow overriding the default ContextProvider. It would have no changes to any current projects, but would allow more flexibility.

function provide({ context, provider }) {
  return ((protoOrTarget,nameOrContext) => {
      ...

      // Standard decorators branch
      nameOrContext.addInitializer(function (this) {
        const ProviderClass = provider || ContextProvider;
        controllerMap.set(this, new ContextProvider(this, {context}));
      });

     ...
  });
}

The end goal here is to have a provided object that is able to update itself. I do not see anyway this is possible without using a custom ContextProvider.

The issue is if I want to bundle logic for controlling a timer, I cant just make a Timer class and provide that as that timer has no way of telling its consumer its updated. In the following example, the consumer will never update because the reference to the object isnt changing. The only way I see this is possible is bundling the timer logic into a ContextProvider.

class Timer {
   value = 0;

  tick() {
     value += 1;
  } 
}

class AppContainer extends LitElement {
  @provide({ context: timerContext })
  timer = new Timer();
  
  constructor() {
    super();
    setInterval(() => {
    timer.tick();
    });
  }
}

class TimerDisplay extends LitElement {
  @consume({ context: timerContext, subscribe?: boolean })
  timer;

  render() {
    return html`Time is ${this.timer.value}`;
  }

@justinfagnani
Copy link
Collaborator

justinfagnani commented Apr 26, 2024

@joezappie I think this is a misuse of ContextProvider. It's not meant to be a general solution for observable state, it's mean to allow passing values down a tree. What you're doing with this.setValue(this, true) is telling all consumers that there's a new Timer, when there isn't. Our intention with context is definitely not to encourage every observable object to be a context provider.

What I would do is make an observable Timer class. There are several options including EventTarget, signals, observables, etc. We have a @lit-labs/preact-signals package this you could use now.

One of the most platform-centric and bare-bones way to be observable is to implement EventTarget. Then the consumer just has to listen to an event and update. I've made a decorator to make this more ergonomic before:

const listeners = new WeakMap<Element, () => void>();

/**
 * A property decorator that subscribes to an event on the property value and
 * calls `requestUpdate` when the event fires.
 */
export const updateOnEvent = (eventName: string) => (target: ReactiveElement, propertyKey: string) => {
  const descriptor = Object.getOwnPropertyDescriptor(target, propertyKey)!;

  const {get, set} = descriptor;
  const newDescriptor = {
    ...descriptor,
    set(this: ReactiveElement, v: EventTarget) {
      let listener = listeners.get(this);
      if (listener === undefined) {
        listeners.set(this, listener = () => this.requestUpdate());
      }
      const oldValue = get!.call(this);
      oldValue?.removeEventListener?.(eventName, listener);
      v?.addEventListener?.(eventName, listener);
      return set!.call(this, v);
    },
  };
  Object.defineProperty(target, propertyKey, newDescriptor);
};

I've update your playground top use this: https://lit.dev/playground/#gist=e3600ffc30ff9359404d4268c59d22a7

@joezappie
Copy link
Author

joezappie commented Apr 26, 2024

@justinfagnani Thank you for the fast reply!

I think what you've shown is an excellent solution to what I'm looking to do. I agree, I always felt weird trying to turn the provider into basically a controller.

I also like that components can subscribe to different events if only certain parts of the controller affect their display. Lots of flexibility there, awesome!

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

No branches or pull requests

3 participants