Git Product home page Git Product logo

stencil-state-tunnel's Introduction

npm

Stencil State Tunnel

This project was built to provide additional functionality to developers working with stencil components and sharing state across components and through slots. The API was inspired by React's Context.

There are many cases within component based application development where you will want to drill state down through props to children and children's children and so on. This project was created in order to alleviate the need for pass props deep through an application's component structure.

There are 2 primary use cases where this tool makes sense.

  1. Building a stencil application and need to store application state in a centralized location.
  2. Building a collection of components that need to share state but have no direct 'line of sight' between the component relationships (ie most likely from using slot).

More information on usage and API can be found in our wiki.

stencil-state-tunnel's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

stencil-state-tunnel's Issues

vNode passed as children has unexpected type. Make sure it's using the correct h() function.

When using state tunnel with stencil 1.3.3 I get vNode passed as children has unexpected type. Make sure it's using the correct h() function. I simply just followed the guide.

This is my tunnel:

import { h } from "@stencil/core";
import { createProviderConsumer } from "@stencil/state-tunnel";

export interface CollapsibleState {
  isOpen: boolean;
  setIsOpen: (fn: (oldVal: boolean) => boolean) => void;
}

export const CollapsibleTunnel = createProviderConsumer<CollapsibleState>(
  {
    isOpen: true,
    setIsOpen: () => false
  },
  (subscribe, child) => (
    <context-provider subscribe={subscribe} renderer={child}></context-provider>
  )
);

Setting up the provider:

export class Collapsible {
  @State()
  isOpen = false;

  render() {
    return (
      <div class="cui-collapsible">
        <CollapsibleTunnel.Provider
          state={{
            isOpen: this.isOpen,
            setIsOpen: fn => {
              this.isOpen = fn(this.isOpen);
            }
          }}
        >
          <slot />
        </CollapsibleTunnel.Provider>
      </div>
    );
  }
}

Using the tunnel:

export class CollapsibleBody {
  render() {
    return (
      <CollapsibleTunnel.Consumer>
        {({ setIsOpen }) => (
          <button onClick={() => setIsOpen(o => !o)}>Toggle</button>
        )}
      </CollapsibleTunnel.Consumer>
    );
  }
}

Types are also not correctly inferred to the Consumer, setIsOpen is typed as any.

Tunnel.Consumer does not work with stencil version 0.16

The only way I could continue to use Tunnel was to use injectProps

basically the following code doesnt work anymore with the latest version:

render () {
  return (<Tunnel.Consumer>{(props) => {
        // props is now always an empty object :(
        ...
     })
  </Tunnel.Consumer>)
}

Doesn't run consumer renderer if no consumerRender is passed

I'm using this in a Stencil/Ionic PWA project, and looking at src/utils/state-tunnel.tsx

function defaultConsumerRender(subscribe: SubscribeCallback<string>, renderer: Function) {
  return <context-consumer
    subscribe={subscribe}
    renderer={renderer}
  />;
}

export function createProviderConsumer<T extends object>(
  defaultState: T,
  consumerRender = defaultConsumerRender // <-- I thought this would work
) {
  // ...
}

I was assuming that I don't need to pass the consumerRender when creating my tunnel because it just uses the defaultConsumerRender:

export default createProviderConsumer<{ foo: string }>({ foo: 'bar' });

However that doesn't work, my renderer never gets called. I need to actually pass my own consumerRender:

export default createProviderConsumer<{ foo: string }>({ foo: 'bar' }, (s, r) => (
  <context-consumer subscribe={s} renderer={r} />
));

Not sure whether that's just an issue with my setup or because i'm importing the distributed/compiled version, and the <context-consumer /> component just isn't able to execute the first Consumer child as a renderer function then?

Anyway, if you find that it's supposed to work, maybe we can get it fixed... otherwise I'd suggest to not give the consumerRender argument a default value (I can submit a PR for that).

Or maybe we could export defaultConsumerRender and then allow to do sth like

import { createProviderConsumer, defaultConsumerRender } from '@stencil/state-tunnel';

export default createProviderConsumer<{ foo: string }>(
  { foo: 'bar' },
  defaultConsumerRender
);

(not sure why that would work though)

Can I override props injected by a tunnel?

Say I have a component,

@Component ({ tag: 'my-message' })
export class MyMessage {
  @Prop() message: string;

  render() {
    return <p>{this.message}</p>;
  }
}

Tunnel.injectProps(MyMessage, ['message']);

Now let's say that the tunnel provides a default value:

export default createProviderConsumer<State>(
  {
    message: "Hello world!",
  },
  (subscribe, child) => <context-consumer subscribe={subscribe} renderer={child} />
);

Should I be able to override the message like this:

const foo = document.createElement('my-message'');
foo.message = "It's a new day!";
document.body.appendChild(foo);

This is a simplified example of course, but I'm finding that I'm not able to do this in my component, and it would be really helpful for testing purposes.

How to test?

I have a component page-first.tsx

@Component({ tag: 'first-page' })
export class FirstPage {
  render = () => (
    <Tunnel.Consumer>
      {(state: State) => <h1>{state.message}</h1>}
    </Tunnel.Consumer>
  );
}

with a tunnel state-tunnel.tsx

export interface State {
  message: string;
}

export default createProviderConsumer<State>(
  { message: '' },
  (s, r) => <context-consumer subscribe={s} renderer={r} />
);

and a spec page-first.spec.tsx

import { FirstPage } from './page-first';

describe('First Page', () => {
  it('should build', () => {
    expect(new FirstPage()).toBeTruthy();
  });
});

When I run the test, I get this error from the state-tunnel.tsx file:

TypeError: state_tunnel_1.createProviderConsumer is not a function

Any hints on why this happens?

How to deal with slots in tunnels?

Hi,

I've been struggling to find a solution to pass the slots into a Tunnel.Consumer.

The following is not working (with is expected?):

<Tunnel.Consumer>
  {(state: State) => {
    console.log(state);
    return (
      <div>
        <slot />
      </div>
    );
  }}
</Tunnel.Consumer>

I did not find a documentation, how passing elements to the consumer can be achieved. What would be the straightforward solution to something like this?

TypeError: Cannot destructure property `children` of 'undefined' or 'null' when attempting to render a consumer

Versions
@stencil/core - 0.12.2
@stencil/state-tunnel - 0.0.7

First, thanks for this work! It seems like it will be useful for us. Hoping that we're just making a stupid usage error, but we're not sure if/what we're doing wrong. Wanted to open it up here in case there was something else going on.

When we attempt to render a Consumer component, we are getting the following error:

TypeError: Cannot destructure property `children` of 'undefined' or 'null'.
    at Consumer (chunk-97f9c476.js:53)
    at h (our-project.core.js:553)
    at OurComponent.render (our-component.js:6022)
    at render (our-project.core.js:630)
    at renderUpdate (our-project.core.js:752)
    at update (our-project.core.js:746)
    at push (our-project.core.js:707)
    at hostElm.s-rc.forEach.cb (our-project.core.js:662)
    at Array.forEach (<anonymous>)
    at render (our-project.core.js:662) 8 "OUR-COMPONENT"

Digging through the code listed in the stack trace a bit, it looks like this might be due to the fact that the generated code calls h() without anything specified in the vnodeData argument. This results in Consumer attempting to destructure null.

The generated code looks like this (notice the 2nd argument is null).

return (h(Tunnel.Consumer, null, ({ error, success }) => ...

This is the section where the error occurs in h():

    if ('function' === typeof nodeName) 
      // nodeName is a functional component
      return nodeName(vnodeData, children || [], utils); /* vnodeData is undefined here. so this becomes Consumer(null)

The render method for OurComponent:

return (
      <Tunnel.Consumer>
        {
          ({ error, success }) => (
            <label class={ourComponentClasses}>
              <input
                class="our-component__control"
                checked={this.checked}
                disabled={this.disabled}
                name={this.name}
                type="radio"
                value={this.value}
              />
              <span
                class={`
                  ${ourComopnentIndicatorClasses}
                  ${error && 'our-component__indicator--error'}
                  ${success && 'our-component__indicator--success'}
                `}
              />
              <span class="our-component__label">{this.label}</span>
            </label>
        )}
      </Tunnel.Consumer>
    );

Thanks again - any ideas are appreciated.

Allow consumers to have props

Consumers currently can't have props, and they create a new DOM element context-consumer which can cause problems for hierarchy/styling/etc since you can't currently apply classes, data attributes, etc to this component.

I propose this is done by doing the following:

  • Allow the Consumer functional component to take any props instead of <{}>
  • Have the consumerRender function take a third argument, props
  • Pass the props of the Consumer functional component to the consumerRender function
  • Update documentation to show the usage of props through destructuring:
export default createProviderConsumer<State>({
    message: 'Hello!'
  },
  (subscribe, child, props) => (
    <context-consumer subscribe={subscribe} renderer={child} ...props />
  )
);

Access consumer outside of render()

Is there any way to access the consumer outside of the render() hook? Something similar to React's this.context, for example:

componentDidLoad() {
  this.context.increment()
}

Note: The docs state "There are other approaches to consuming the information" - which approaches exactly?

Tunnel per instance?

According to the examples, createProviderConsumer() is created statically in a file, e.g. data-tunnel.tsx. This basically means tunnels are created on class level rather than on instance level. While this is totally the way to go for single page apps that only have one instance of a root component, e.g. my-app, it makes it hard when building a component library, where every component instance should have its own tunnel and multiple instances of that component can coexist. In other words:

Current - these two instances of "my-select" now share the same tunnel and mix up the state. This leads to unexpected behavior:

<my-select> <!-- imports "data-tunnel.tsx" -->
  <my-option> <!-- imports "data-tunnel.tsx" -->
</my-select>

<my-select> <!-- also imports "data-tunnel.tsx" -->
  <my-option> <!-- also imports "data-tunnel.tsx" -->
</my-select>

Expected - it would be great if the developer could decide whether to have tunnels on instance level or on class level. Every time an instance of "my-select" is created, also a corresponding tunnel is created. So each parent has its own tunnel and its child components automatically consume that particular tunnel:

<my-select> <!-- creates a new tunnel -->
  <my-option> <!-- consumes that new tunnel -->
</my-select>

<my-select> <!-- creates another new tunnel which does not interfere with the my-select above -->
  <my-option> <!-- consumes that "another" new tunnel -->
</my-select>

Exporting Web Components

Sorry if this is a bit of a silly question,
But i'm just getting my head around Stencil.
If I was to use this library when developing a set of related web components that I want teams using React & Vue to consume, would they need to install this as an external dependency, or would it be bundled by the Stencil compiler ?

Props are undefined during e2e tests

I use the stencil state tunnel for passing message-texts through all of the components in my widget build with stencil. While e2e-testing a component that renders child-components which consume messages from my state tunnel these are undefined because i can't define a tunnel for my e2e tests nor i can set manually the props of the rendered child components.

Has anybody an idea for fixing this? Is it possible to define/mock a state tunnel in e2e tests?

btw. i am using the Tunnel.injectProps method in my components.

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.