4

While I understand this is probably a terrible practice, I need to build StencilJS component such that inside render(), I don't want to render component tag itself due to already existing style guide and it expect DOM to be constructed in certain way. Here is what I'm trying to achieve - component code (from HTML or within another component):

<tab-header-list>
  <tab-header label="tab 1"></tab-header>
  <tab-header label="tab 2"></tab-header>
</tab-header-list>

when rendered, I want generated DOM to be something like:

<tab-header-list>
  <ul>
    <li>tab 1</li>
    <li>tab 2</li>
  </ul>
</tab-header-list>

so inside tab-header-list render() function, I'm doing

return (
  <ul>
    <slot/>
  </ul>
);

and I can do this inside tab-header render() function

@Element() el: HTMLElement;
@Prop() label: string;

render() {
  this.el.outerHTML = `<li>${this.label}</li>`;
}

to get what I want but how can I do this with TSX? (for simplicity sake, above code is really simple but what I really need to build is lot more complicated li tag with events etc so I would like to use TSX)

Tried to store DOM to variable but I'm not sure how I can assign it as this.el (outerHTML seem to be only way I can come up with, but I feel there must be better way)

@Element() el: HTMLElement;
@Prop() label: string;

render() {
  var tabheaderDOM = (<li>{this.label}</li>);

  // how can I assign above DOM to this.el somehow?
  //this.el.outerHTML = ?
}

I appreciate any help I can get - thanks in advance for your time!

tomokat
  • 142
  • 1
  • 8

2 Answers2

4

Unfortunately, you can't use custom elements without tags, but there is a workaround for it:

You can use Host element as reference to the result tag.

render () {
  return (
    <Host>....</Host>
  )
}

Then in your stylesheet you can set the display property for it:

:host {
  display: contents;
}

display: contents causes an element's children to appear as if they were direct children of the element's parent, ignoring the element itself

Beware: it doesn't work in IE, opera mini... https://caniuse.com/#feat=css-display-contents

UPD:

If you are not using the shadowDOM then you need to replace :host by the tag name like:

tab-header {
  display: contents;
}
Ivan Burnaev
  • 2,690
  • 18
  • 27
  • Thanks for your quick reply. I forgot to tell you that I was using this with "shadow: false" - since I need to reflect style guide's style (which is loaded as global css) so your solution doesn't seem to work in that case (totally my fault - I should've add that in my question in the first place) Now that I rethink what I'm trying to do, maybe it is just super messy - so most likely I should just construct tab-headings inside tag-heading-list and take dataObject to customize tab heading as required. Once again, thank you for your quick reply with this many detail! – tomokat Jul 23 '20 at 13:59
  • Ah, it's not a big deal. You need to replace `:host` by a tag name. – Ivan Burnaev Jul 23 '20 at 14:09
  • Updated my initial answer – Ivan Burnaev Jul 23 '20 at 14:11
  • Thanks! Surrounding TSX component inside tab-header with ... seem to did the trick! – tomokat Jul 23 '20 at 15:51
2

Functional components might be able to help you achieve this. They are merely syntactic sugar for a function that returns a TSX element, so they are completely different to normal Stencil components. The main difference is that they don't compile to web components, and therefore only work within TSX. But they also don't result in an extra DOM node because they simply return the template that the function returns.

Let's take your example:

@Element() el: HTMLElement;
@Prop() label: string;

render() {
  this.el.outerHTML = `<li>${this.label}</li>`;
}

you could write it as a functional component:

import { FunctionalComponent } from '@stencil/core';

interface ListItemProps {
  label: string;
}

export const ListItem: FunctionalComponent<ListItemProps> = ({ label }) => (
  <li>{label}</li>
);

and then you can use it like

import { ListItem } from './ListItem';

@Component({ tag: 'my-comp' })
export class MyComp {
  render() {
    return (
      <ul>
        <ListItem label="tab 1" />
        <ListItem label="tab 2" />
      </ul>
    );
  }
}

Which will render as

<ul>
  <li>tab 1</li>
  <li>tab 2</li>
</ul>

Instead of a label prop you could also write your functional component to accept the label as a child instead:

export const ListItem: FunctionalComponent = (_, children) => (
  <li>{children}</li>
);

and use it like

<ListItem>tab 1</ListItem>

BTW Host is actually a functional component. To find out more about functional components (and there limitations), see https://stenciljs.com/docs/functional-components.

Simon Hänisch
  • 4,740
  • 2
  • 30
  • 42
  • here, I think I want to continue using component since list-item do generate DOM and actual "li" tag is quite complicated, so instead of hiding it under Functional Component, using "display: content" as accepted answer seem to be what I was looking for. Btw, I really love your answer in the another question and I did accept that answer, so thanks again for your great solution - you definitely show me how powerful Functional component can be! :) – tomokat Jul 27 '20 at 12:56