Page MenuHomePhabricator

Investigation - How to have a parent component manage the state of the component in its default slot
Closed, ResolvedPublic

Description

Current proof of concept with highlight of current problems: https://github.com/wmde/wikit/pull/358/files#diff-e7b8ec01eeebbde10914a6c7eb262cd5642db07dac408bcffc8466da0e7cc657R14-R40

timebox: collect feedback over max 1 week and then 1 day for finalizing decision

Event Timeline

Michael subscribed.

Turns out I was looking more into this after all

Using vuejs dependency injections API provide/inject in practice

In the design system (and query builder) we need a component to toggle between two options. To make it easier to adjust the contents of the individual options, we decided to go with one wrapping component that get's the individual buttons in its default slot. Like so:

<ToggleButtonGroup>
    <ToggleButton>
    <ToggleButton>
    <ToggleButton>
</ToggleButtonGroup>

However, we also would like the wrapping component, i.e. ToggleButtonGroup, to do the internal state management for us. That creates a challenge, because usually components do not have meaningful access to the content in their slots, let alone listen to their events or tell them which isActive prop they are supposed to have.

That being said, it has to be possible, because that is exactly the functionality that vuetify provides:

<v-btn-toggle v-model="toggle_exclusive">
    <v-btn>
        <v-icon>mdi-format-align-left</v-icon>
    </v-btn>

    <v-btn>
        <v-icon>mdi-format-align-center</v-icon>
    </v-btn>

    <v-btn>
        <v-icon>mdi-format-align-right</v-icon>
    </v-btn>

    <v-btn>
        <v-icon>mdi-format-align-justify</v-icon>
    </v-btn>
</v-btn-toggle>

The scavenger hunt to figure out what they are doing lead through vuetify's registrable mixin, used in their groupable mixin used in their VBtn component with the methods defined in the ItemGroup component. The solution is that they are using provide/inject to register the <v-btn> buttons in the slot inside the <v-btn-toggle>.

Our own solution is somewhat less sophisticated, but still has to make use of the provide/inject API that vuejs provides for dependency injection (vue2 docs, vue3 docs).

In the Proof of Concept for the ToggleButtonGroup, we are providing a listener for each button and a method to get the currently selected value for the group:

export interface ToggleButtonGroupInjection {
	groupValue: ( () => string ) | null;
	toggleListener: ( ( value: string ) => void ) | null;
}
export default Vue.extend( {
	name: 'ToggleButtonGroup',
	provide(): ToggleButtonGroupInjection {
		return {
			groupValue: (): string => this.value,
			toggleListener: ( event: string ): void => {
				this.$emit( 'input', event );
			},
		};
	},
	// props etc.
} );

That is then injected in the ToggleButton:

import { ToggleButtonGroupInjection } from '@/components/ToggleButtonGroup.vue';
export default ( Vue as VueConstructor<Vue & ToggleButtonGroupInjection> ).extend( {
	name: 'ToggleButton',
	methods: {
		onClick(): void {
			if ( this.toggleListener !== null ) {
				this.toggleListener( this.value );
				return;
			}
			/**
			 * only emitted when not use as part of a ToggleButtonGroup
			 */
			this.$emit( 'click' );
		},
	},
	inject: {
		groupValue: { default: null },
		toggleListener: { default: null },
	} as Record<keyof ToggleButtonGroupInjection, object>,
	computed: {
		buttonIsActive(): boolean {
			if ( this.groupValue !== null ) {
				return this.groupValue() === this.value;
			}
			return this.isActive;
		},
	},
    // props etc.
} );

Note the line

export default ( Vue as VueConstructor<Vue & ToggleButtonGroupInjection> ).extend( {

That is needed so that TypeScript knows about these two fields on this.

That way it is possible to directly transfer knowledge between a component and the children in its slots.

Next steps: Implementing T271788 by completing the Pull Request so that it checks all the ACs in the ticket.