StencilJs 學習之 JSX

2023-06-21 09:00:24

Stencil 元件使用 JSX 渲染,這是一種流行的宣告式模板語法。每個元件都有一個渲染函數,它返回在執行時渲染到 DOM 的元件樹。

基礎用法

render 函數用於輸出將繪製到螢幕上的元件樹。

class MyComponent {
    render() {
        return (
            <div>
                <h1>Hello World</h1>
                <p>This is JSX!</p>
            </div>
        );
    }
}

在這個例子中,我們返回了一個 div ,它有兩個子元素 h1p

Host Element

如果你想修改宿主元素本身,比如向元件本身新增一個類或一個屬性,可以使用 Host 元件。

資料繫結

元件經常需要渲染動態資料。要在 JSX 中執行此操作,請使用 {}

render() {
  return (
    <div>Hello {this.name}</div>
  )
}

如果你熟悉 ES6 模板變數,JSX 變數和 ES6 非常相似,只是沒有 $ 符號。

//ES6
`Hello ${this.name}`

//JSX
Hello {this.name}

條件

如果你想根據不同的條件渲染不同的內容,可以使用 if/else 來實現

render() {
  if (this.name) {
    return ( <div>Hello {this.name}</div> )
  } else {
    return ( <div>Hello, World</div> )
  }
}

此外,可以使用 JavaScript 的三元運運算元來建立內聯條件

render() {
  return (
    <div>
    {this.name ? <p>Hello {this.name}</p> : <p>Hello World</p> }
    </div>
  );
}

請注意:Stencil 重用 DOM 元素以獲得更好的效能。請看下面的程式碼:

{
    someCondition ? (
        <my-counter initialValue={2} />
    ) : (
        <my-counter initialValue={5} />
    );
}

上面的程式碼與下面的程式碼完全相同:

<my-counter initialValue={someCondition ? 2 : 5} />

因此,如果某些條件發生了變化,my-counter的內部狀態不會被重置,它的生命週期方法(如 componentWillLoad())也不會被觸發。相反,條件語句只會觸發同一個元件的更新。

如果你想在一個條件語句中銷燬並重新建立一個元件,你可以指定 key 屬性。這告訴 Stencil,這些元件實際上是不同的兄弟元件:

{
    someCondition ? (
        <my-counter key="a" initialValue={2} />
    ) : (
        <my-counter key="b" initialValue={5} />
    );
}

這樣,如果某些條件發生變化,你會得到一個新的 my-counter 元件範例,它具有新的內部狀態,同時也將會同步執行生命週期 componentWillLoad()componentDidLoad()

Slots

元件通常需要在其元件樹的特定位置動態渲染子元件,允許開發人員在使用我們的元件時提供子內容,我們的元件將子元件放置在適當的位置。

要做到這一點,您可以在 my-component 中使用 Slot 標籤。

// my-component.tsx
render() {
  return (
    <div>
      <h2>A Component</h2>
      <div><slot /></div>
    </div>
  );
}

然後,如果使用者在建立元件 my-component 時傳遞子元件,那麼 my-component 將把該元件放在上面第二層的 div 中:

render(){
  return(
    <my-component>
      <p>Child Element</p>
    </my-component>
  )
}

slot 可以增加 name 屬性,來決定內容的輸出位置:

// my-component.tsx

render(){
  return [
    <slot name="item-start" />,
    <h1>Here is my main content</h1>,
    <slot name="item-end" />
  ]
}

render(){
  return(
    <my-component>
      <p slot="item-start">I'll be placed before the h1</p>
      <p slot="item-end">I'll be placed after the h1</p>
    </my-component>
  )
}

Dealing with Children

JSX 中節點的子節點在執行時對應於一個節點陣列,無論它們是通過 array.prototype.map 跨陣列建立的,還是直接在 JSX 中宣告為兄弟節點。這意味著在執行時,下面兩個頂級
div 的子元素(.Todo-one 和.todo-two)的表示方式相同:

render() {
  return (
    <>
      <div class="todo-one">
        {this.todos.map((todo) => (
          <span>{ todo.taskName }</span>
        )}
      </div>
      <div class="todo-two">
        <span>{ todos[0].taskName }</span>
        <span>{ todos[1].taskName }</span>
      </div>
    </>
  )
}

如果這個子元素陣列是動態的,即任何節點都可以被新增、刪除或重新排序,那麼最好為每個元素設定一個唯一的 key 屬性,如下所示:

render() {
  return (
    <div>
      {this.todos.map((todo) =>
        <div key={todo.uid}>
          <div>{todo.taskName}</div>
        </div>
      )}
    </div>
  )
}

當子陣列中的節點被重新排列時,Stencil 會努力在渲染時保留 DOM 節點,但它不能在所有情況下都這樣做。設定一個 key 屬性可以讓 Stencil 確保在渲染時能夠匹配新舊子節點,從而避免
不必要地重新建立 DOM 節點。

不要使用陣列索引或其他非唯一值作為鍵。嘗試確保每個子節點都有一個不變的 key,並且在其所有兄弟節點中是唯一的。

處理使用者輸入

Stencil 使用原生的 DOM 事件。

下面是一個處理按鈕點選的例子。注意箭頭函數的使用。

...
export class MyComponent {
  private handleClick = () => {
    alert('Received the button click!');
  }

  render() {
    return (
      <button onClick={this.handleClick}>Click Me!</button>
    );
  }
}

這是另一個監聽輸入變化的例子。注意箭頭函數的使用。

...
export class MyComponent {
  private inputChanged = (event: Event) => {
    console.log('input changed: ', (event.target as HTMLInputElement).value);
  }

  render() {
    return (
      <input onChange={this.inputChanged}/>
    );
  }
}

複雜的模板內容(Complex Template Content)

到目前為止,我們已經看到了如何只返回一個根元素的例子。我們也可以在根元素中巢狀元素

在元件有多個「頂級」元素的情況下,render 函數可以返回一個陣列。注意 div 元素。

render() {
  return ([
  // first top level element
  <div class="container">
    <ul>
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </ul>
  </div>,

  // second top level element, note the , above
  <div class="another-container">
    ... more html content ...
  </div>
  ]);
}

或者你可以使用 Fragment 函陣列件,在這種情況下你不需要新增逗號:

import { Fragment } from '@stencil/core';
...
render() {
  return (<Fragment>
    // first top level element
    <div class="container">
      <ul>
        <li>Item 1</li>
        <li>Item 2</li>
        <li>Item 3</li>
      </ul>
    </div>

    <div class="another-container">
      ... more html content ...
    </div>
  </Fragment>);
}

也可以使用 innerHTML 直接將內容內聯到元素中。例如,當動態載入一個 svg,然後想要在 div 中渲染它時,這就很有用了。這就像在普通的 HTML 中一樣:

<div innerHTML={svgContent}></div>

獲取 DOM 元素的參照

jsx 中使用 ref 屬性來獲取 dom 的參照,範例如下

@Component({
    tag: "app-home",
})
export class AppHome {
    textInput!: HTMLInputElement;

    handleSubmit = (event: Event) => {
        event.preventDefault();
        console.log(this.textInput.value);
    };

    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <label>
                    Name:
                    <input
                        type="text"
                        ref={(el) => (this.textInput = el as HTMLInputElement)}
                    />
                </label>
                <input type="submit" value="Submit" />
            </form>
        );
    }
}

避免共用 JSX 節點

在 jsx 中應該避免共用 jsx 節點,每一個 jsx 節點應該都是唯一的,這是因為在再次渲染時會遇到問題。

@Component({
  tag: 'my-cmp',
})
export class MyCmp {

  render() {
-    const sharedNode = <div>Text</div>;
    return (
      <div>
-        {sharedNode}
-        {sharedNode}
+        <div>Text</div>
+        <div>Text</div>
      </div>
    );
  }
}

或者,可以建立一個工廠函數來返回一個通用的 JSX 節點,因為返回值將是一個唯一的範例。 範例如下:

@Component({
    tag: "my-cmp",
})
export class MyCmp {
    getText() {
        return <div>Text</div>;
    }

    render() {
        return (
            <div>
                {this.getText()}
                {this.getText()}
            </div>
        );
    }
}

結束語

至此,我們已經基本把 StencilJs 的相關基礎知識已經學習的差不多了,在下一個章節中將會使用之前學習到的知識來開發一個常用的元件。
由於我們只是使用 StencilJs 來開發 web component 元件,其它不想關的知識(router)便不再講解。