Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass functions to stencil component

Is it possible to pass a function to a stencilJs component?

Something like:

@Prop() okFunc: () => void;

I have a modal and want to dynamically call a passed function on the Ok button clicked in the modal footer, like an onClick on a normal HTML button.

like image 560
mahau Avatar asked Mar 20 '18 15:03

mahau


2 Answers

Yes, you can. It is just a normal @Prop() declaration and plays very nicely in TSX. However...

As noted in another answer and comments, if you are consuming a Stencil component in plain ol' HTML, you will not be able to use Stencil's attribute-prop binding to pass a function (or any non-scalar value) to a prop via an HTML attribute.

Use the DOM API

This means that you have to interact with the DOM if you want to attach events or pass function props to Stencil components. Once your component makes it into the DOM, there is really nothing special about it compared to any other Custom Element.

Without JSX or another template DSL (e.g. Angular), you will need to attach events and set your function- or object-reference props with the JavaScript DOM API:

const componentInstance = document.querySelector('my-stencil-component')

// This works and will trigger a rerender (as expected for a prop change)
componentInstance.someFunc = (...) => { ... }

// This works for any event, including custom events fired from Stencil's EventEmitter
componentInstance.addEventListener('myCustomEvent', (event: MyCustomEvent) => { ... })

If you absolutely must do this in your HTML document for some reason:

<my-stencil-component ... >...</my-stencil-component>
<script>
  var componentInstance = document.currentScript.previousElementSibling
  componentInstance.someFunc = function(...) { ... }
</script>

Why do I have to do this?

It's important to realize that Properties ≠ Attributes. Props are JavaScript Properties, in this case, properties of the DOM object representing an element. Attributes are XML attributes, although HTML attributes have some unique characteristics and behave slightly differently than typical XML.

Stencil will automatically "bind" HTML attributes to properties for you where possible - in particular, for scalar values (boolean, number, string). Object references, and therefore functions, cannot be used as the value of an attribute. Technically, only strings can be attribute values, but Stencil is smart enough to convert string to other another scalar type (boolean or number) when you specify the type of your @Prop().


Other Solutions

I developed a solution for my team to bind attributes containing JSON to Stencil properties using a MutationObserver. It basically watches for a special attribute bind-json, then maps attributes starting with json- onto the corresponding camelCase DOM properties. Usage looks like this:

<my-stencil-component
  bind-json
  json-prop-name='{ "key": "value" }'>
</my-stencil-component>

With the MutationObserver in place, this is identical to:

const componentInstance = document.querySelector('my-stencil-component')
componentInstance.propName = JSON.parse(componentInstance.getAttribute('json-prop-name'))

However, there really is not a satisfying solution for binding functions in plain HTML. It really just can't be done without some sort of ugly hack like eval described in another comment. Not only does that pollute your component's API, it's problematic for all kinds of other reasons I won't get into here and its use will automatically fail your app in practically any modern security check.

In our Storybook stories we bind callback function definitions to the window, and use <script> tags and document.currentScript or querySelector to pass the function props and event bindings to the component instance:

const MyStory = ({ ... }) => {
  window.myStoryFunc = () => { ... }
  window.myStoryClickHandler = () => { ... }
  return `
    <my-stencil-component ... >...</my-stencil-component>
    <script>
      const componentInstance = document.currentScript.previousElementSibling
      componentInstance.someFunc = window.myStoryFunc
      componentInstance.addEventListener('click', window.myStoryClickHandler)
    </script>
  `
}
like image 76
Jon Hieb Avatar answered Oct 04 '22 21:10

Jon Hieb


You can just add a @Prop() someFunc: Function to any component and pass it from the outside like <any-component someFunc={() => console.log('coming from the outside')} />

Within anyComponent just check if (this.someFunc) { this.someFunc() }

like image 27
Schadenn Avatar answered Oct 04 '22 22:10

Schadenn