Render a component within a Widget. (Using select-kit components within plugin code)

If we successfully define a select-kit component within a plugin we are easily able to use it within a *.hbs template file using its pluginApiIdentifiers however, in order to attach that component within a widget using JS code I am not able to make it work. For instance:

I have:

import DropdownSelectBoxComponent from 'select-kit/components/dropdown-select-box';

export default DropdownSelectBoxComponent.extend({
  pluginApiIdentifiers: ['header-profile-dropdown'],
  ...
  
  autoHighlight() { ... },

  computeContent() {
    ...
    return items;
  },

  mutateValue(id) { ... }
});

Then I can go to any template I want to overwrite and write like:

(taking as an example the discovery template)

...
<div id="list-area">
        {{header-profile-dropdown}}
...

And the snippet above will work flawlessly, BUT how I will include that component into a widget like:

    api.reopenWidget('user-dropdown', {
        html(attrs, state) {
          if (this.site.mobileView) {
            return h('div.profile-dropdown-container', [
              this.attach('header-notifications', attrs),
              --> I NEED ATTACH THE COMPONENT HERE <--
            ]);
          }

          ...
        }
      });

NOTE: much of the code has been skipped to keep the post brief. Feel free to ask for the full code if needed.

PD: I have already tried to describe this issue at How to attach a component to a widget with just markup? but looks like it was a too complicated explanation so I am trying to reduce the information here to make it clear and try to find an answer.

I think it was a matter of ask myself the right question: “How to render a component within a widget?” that’s why I updated the topic title. I have found few results already such as: How to render Component inside Widget? - #2 by eviltrout I will take a look and post any useful finding if it is needed.

Sadly enough looks like it is not possible, simply put we need to find a way to create a widget instead to render what we need to and render a widget within a widget, cause using the connector there are a lot of limitations. Check also: Rendering Component in decorativeWidget API

same issue here.
this is really terrible frontend code that uses widgets and ember components in the same page, it’s really hard to revamp and maintain.
can’t understand what’s the logic

An example of this is available in Discourse itself:

https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6#L362

I wasn’t there when widgets were created, but as far as I can tell, it was created for performance reasons at a time where frontend and especially mobile frontend had huge performance issues. And for dev simplicity it was added the possibility to inject components into widgets.

You got to understand that Discourse carries some history and we can’t start from scratch every year.

기록을 위해 말씀드리면, 위젯 내에서 Ember 컴포넌트를 렌더링하기 위한 새로운 시스템을 도입했습니다. 자세한 내용은 다음 커밋 메시지에서 확인하실 수 있습니다:

코어 코드베이스에서 RenderGlimmer를 검색하면 사용 예시를 찾을 수 있습니다.

이거 정말 좋은데요 @David! … 그런데 왜 이걸 가져오는 데 문제가 생기는 걸까요:

import { hbs } from "ember-cli-htmlbars";
Error: Could not find module `ember-cli-htmlbars`

Theme Component에서 이걸 사용하려고 하면 오류가 나는데,

(최신 tests-passed를 사용하고 있습니다)

음… 인라인 컴파일이 테마 컴포넌트에서는 (아직) 작동하지 않을 가능성이 매우 높습니다. 플러그인에서는 작동해야 합니다(이번 주 기준). 테마 컴포넌트 지원을 추가할 수 있는 방법을 찾아보겠습니다 :eyes:

이렇게 하면 작동할 것입니다:

다음 주에 병합되도록 노력하겠습니다.

훌륭한 진전입니다. 이걸 쓸 곳이 있어서 기대됩니다 :+1:t2:

병합했습니다. 진행 상황 알려주세요 @merefield

빙고! 이제 내 TC 기반 위젯에 Component가 표시되고, 데이터도 올바르게 채워졌네요. 정말 멋집니다! :rocket:

영국 공휴일을 넘겨서 이렇게 빠르게 처리해 주셔서 감사합니다!

위젯이 클릭 이벤트를 처리한다는 건 알지만, 내 Component에 액션을 트리거하는 이벤트가 있다면, 그 액션을 위젯 내에서 어디에서 처리할 수 있을까요?

오, 데이비드, 방금 예시를 봤어요. 공부하고 있습니다!

네, 그게 아마 가장 좋은 참고 자료일 거예요. 코어에서 아직 제대로 액션을 전달한 적이 없는 것 같으니, 문제가 생기면 알려주세요.

그 테스트를 다시 살펴보니, actionForComponentToTrigger() 함수 위에 @bind가 필요할 것 같아요. 그래야 함수 안에서 this가 올바르게 작동할 테니까요.

이 정도까지는 작동합니다:

    contents.push(
      new RenderGlimmer(
        this,
        "div.tag-chooser-component",
        hbs`<TagChooser @id="list-with-tags"  @tags={{@data.chosen}} @onChange={{action @data.onChangeUpdateTagSet}}/>
        <button onClick={{@data.actionForClick}} label="blah"/>`,
        {
          chosen: this.chosen,
          onChangeUpdateTagSet: this.onChangeUpdateTagSet,
          actionForClick: this.actionForClick
        }
      ),
    );
    return contents;
  },

  @bind
  onChangeUpdateTagSet(chosen) {
    this.chosen.push(chosen.lastObject);
  },

  @bind
  actionForClick () {
    console.log(this.chosen)
  }
});

onChangeUpdateTagSet은 변경 사항이 발생 시 호출되고, actionForClick은 버튼 클릭 시 호출됩니다.

그러나 태그 선택기(Tag Chooser)에 데이터를 유지하는 데 어려움을 겪고 있습니다.

onChange에 커스텀 액션을 도입하는 즉시, 컨트롤이 UI에서 선택된 태그를 최신 상태로 유지하지 못합니다(커스텀 변수에는 올바른 값이 포함되어 있음에도 불구하고).

onChange에 커스텀 액션을 포함하지 않으면 컴포넌트 UI가 다시 정상 작동하지만, 제 집합(set)을 채울 수 없습니다. :thinking:

onChangeUpdateTagSet 함수에 this.scheduleRerender()를 추가해 보았지만, 이는 undefined로 표시됩니다. 분명 스코프에 있지 않은 것 같습니다.

위젯에 클릭 이벤트를 추가했는데, 그러면 올바른 데이터가 표시되도록 새로고침이 됩니다.

그러나 바인딩이 한 방향만 작동하는 것 같아서 외부 데이터를 컴포넌트로 업데이트하는 대신, 컴포넌트로 새 데이터를 푸시하는 것만 가능합니다.

바인딩을 양방향으로 만들 수 있는 방법이 있을까요?

이것보다는 widget 'state’를 사용해야 할 것 같습니다. this.chosen에 직접 접근하는 방식은 권장되지 않습니다. 위젯 인스턴스는 수명이 짧고 '무상태(stateless)'이기 때문입니다. 재렌더링 시마다 새로운 인스턴스가 생성되므로 this.chosen이 초기화됩니다.

this.state가 change 함수 내에서 undefined라서 상황이 좀 복잡해졌나요?

(전달할 수는 있지만, 그렇게 하면 컴포넌트에서 선택된 항목을 알려주는 데이터를 덮어쓰게 됩니다.

  onChangeUpdateTagSet: this.onChangeUpdateTagSet(this.state),

)

위젯 코드를 더 공유해 주실 수 있나요? 기억이 맞다면, 상태(state)가 작동하려면 buildKey와 defaultState() 메서드를 정의해야 합니다.

MountWidget 테스트에 이런 종류의 예시를 추가해 볼 수 있을지 확인해 보겠습니다 :eyes: