9

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.

Patrick Mevzek
  • 10,995
  • 16
  • 38
  • 54
mahau
  • 91
  • 1
  • 4
  • just found out, that one can do somthing like this: `@Prop() okFunc: string;` then in the footer of the Component ` this.okClicked()} text="Ok">` and `okClicked = () => { eval(this.okFunc); }` – mahau Mar 20 '18 at 16:13
  • `okClicked = () => { var func = ( new Function( 'return ' + this.okFunc ) )(); func(); }` is better than `eval(this.okFunc);` – mahau Mar 20 '18 at 16:54

2 Answers2

8

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>
  `
}
Jon Hieb
  • 760
  • 8
  • 9
0

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

Schadenn
  • 854
  • 1
  • 9
  • 20
  • I can't pass a function from vue into a stencil component. I means can't assign function into ?What to do on this situation!? it takes myVueFunction as string. – Suriya Kumar Jan 25 '19 at 14:46
  • Is there any solution for my situation!? – Suriya Kumar Feb 05 '19 at 15:03
  • @SuriyaKumar If you use `` you'll get the return value of `myVueFunction` instead of the actual function. To pass the function remove the `()` - so you don't call the function at the time you want to pass it. Correct solution: `` – Schadenn Feb 11 '19 at 12:39
  • 2
    above solution works only in JSX (React) . As Stencil is universal web component this is not the correct solution. – Anup B Aug 23 '20 at 11:54