microsoftgraph / microsoft-graph-toolkit

Authentication Providers and UI components for Microsoft Graph 🦒
https://docs.microsoft.com/graph/toolkit/overview
Other
944 stars 302 forks source link

Add support for template cascading #315

Open nmetulev opened 4 years ago

nmetulev commented 4 years ago

Description

The toolkit components should allow templates to cascade to children components to avoid multiple levels of re-templating.

Rationale

For example, to template parts of the person-card, the current approach forces the developer to define a template in a template. This also makes it difficult to use the templateRendered event as the developer needs to assign the event multiple times at each level.

This proposal simplifies defining templates to children components by allowing the developer to define them higher up.

Here is an example on how to update the additional-details template on a mgt-person-card today:

<mgt-person ...>
    <template data-type="person-card">
        <mgt-person-card inherit-details>
            <template data-type="additional-details">
                <div>My additional details</div>
            </template>
        </mgt-person-card>
    </template>
    <template data-type="loading">
        <div>loading...</div>
    </template>
</mgt-person>
  1. The developer defines a person-card template on mgt-person
  2. Inside the template, they create a new mgt-person-card.
  3. Inside the new mgt-person-card, the developer defines a new additional-details template

Note: I've also included a loading template in this example for completeness which will become relevant in the solutions below.

Preferred Solution

I've been thinking about the solution to this problem for a while now and I've outlined how my proposed solution has evolved here

Approach 1

This approach allows all templates to cascade down to the children of the component. Any template that is defined on the parent component would be passed down as templates to all the children components. :

<mgt-person ...>
    <template data-type="additional-details">
        <div>My additional details</div>
    </template>
    <template data-type="loading">
        <div>Loading</div>
    </template>
</mgt-person>

In this scenario, the mgt-person-card would get all templates defined for the parent mgt-person.

This approach is very simple and straightforward. It also allows templates to be defined at the root element and to cascade to any component in the tree. However, this approach also introduces several issues:

I believe these issues make this approach a non-starter and that is why I continued thinking about the problem.

Approach 2

This approach defines a new attribute on the template element: data-part. The data-part attribute values would be defined by the parent. For example, templating just the additional-details template of the person card in the person component would look like this:

<mgt-person ...>
    <template data-type="additional-details" data-part="person-card">
        <div>My additional details</div>
    </template>
    <template data-type="loading">
        <div>Loading</div>
    </template>
</mgt-person>

Where mgt-person has mapped the value person-card to the mgt-person-card and therefore all templates with data-part="person-card" would cascade down to the mgt-person-card.

This solution is not much more complex than solution 1 but addresses the issues from solution 1 by scoping the template to a specific part. However, templates only cascade a single level.

Approach 2.5

If we want to take this even further and solve the cascading issue of solution 2, the data-part attribute could act as a selector (similar to CSS selectors).

For example, imagine the scenario of templating the person-card inside a person inside the agenda component. Ex:

<mgt-agenda...>
    <template data-type="additional-details" data-part="attendee > person-card">
        <div>My additional details</div>
    </template>
    <template data-type="loading" data-part="attendee">
        <div>Loading</div>
    </template>
</mgt-person>

Here, data-part="attendee > person-card" applies the template to the mgt-person-card inside the mgt-person component used for the attendees in the agenda.

And if we wanted to have the flexibility of Solution 1, data-part="*" could act as a selector for all components.

What about the templateRendered event?

So far, we have not addressed how the templateRendered event would work. Today, the developer needs to register the event multiple times on the component:

mgtPerson.addEventListener('templateRendered', e=> {

  let personCard = e.details.element.querySelector('personCard');
  personCard.addEventListener('templateRendered', e => {

    let templateType = e.detail.templateType; // the data-type of the template
    let dataContext = e.detail.context; // the data context passed to the template
    let element = e.detail.element; // the root element of the rendered template

    let button = element.querySelector('button');
    if (button) {
      button.addEventListener('click', () => {});
    }

  }
});

The solution here is for the event to bubble up through the children components and contain the data-part attribute in its details so the developer can confidently identify which template was rendered, Here is how I'd rewrite the event listener:

mgtPerson.addEventListener('templateRendered', e=> {

    let templateType = e.detail.templateType; // the data-type of the template
    let dataContext = e.detail.context; // the data context passed to the template
    let element = e.detail.element; // the root element of the rendered template
    let part = e.detail.templatePart // the data-part selector for the template
    let target = e.target // the actual component that fired the event

    let button = element.querySelector('button');
    if (button) {
      button.addEventListener('click', () => {});
    }
});
michael-hawker commented 4 years ago

Another approach could be about referencing templates (so they could be re-used across the document), similar to how resources work for XAML templates. 😋

    <template name="person-card-template">
        <mgt-person-card inherit-details>
            <template data-type="additional-details">
                <div>My additional details</div>
            </template>
        </mgt-person-card>
    </template>
    <template name="loading-template">
        <div>loading...</div>
    </template>
...
<mgt-person template-loading="loading-template" template-person-card="person-card-template"...>
</mgt-person>
<mgt-agenda template-loading="loading-template">
</mgt-agenda>
nmetulev commented 4 years ago

Yeah, template referencing also makes sense in addition to cascading. I don't think it solves the issue alone as you still have to define template in a template

shweaver-MSFT commented 4 years ago

After a good chat around the various approaches, we have decided to go with approach 2, with data-part being a component tag name (e.g. mgt-person-card). We can then use that to create a concept of parentTemplates which may be referenced from child components during rendering.