mirror of
https://github.com/zoriya/v10.git
synced 2026-08-05 05:37:21 +00:00
WIP: refactor(html): implement hook-style component architecture for PlayButton and MuteButton
- Create generic toConnectedHTMLComponent factory with hooks pattern - Implement usePlayButtonState/Props/Events hooks for state, attributes, and event handling - Implement useMuteButtonState/Props/Events hooks for mute functionality - Replace old connected/ components with new hook-style architecture - Fix button interactivity by using MediaPlayButton/MediaMuteButton base classes - Maintain backward compatibility through connected-with-defaults/ registration - Components now mirror React hooks architecture with separated concerns 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
Christian Pillsbury
co-authored by
Claude
parent
38215df70b
commit
c54945fd58
@@ -0,0 +1,65 @@
|
||||
import { toConnectedHTMLComponent, StateHook, PropsHook, EventsHook } from '../utils/component-factory';
|
||||
import { MediaMuteButton } from './ui/media-mute-button';
|
||||
|
||||
/**
|
||||
* MuteButton state hook - equivalent to React's useMuteButtonState
|
||||
* Handles media store state subscription and transformation
|
||||
*/
|
||||
export const useMuteButtonState: StateHook<{ muted: boolean; volumeLevel: string }> = {
|
||||
keys: ['mediaMuted', 'mediaVolumeLevel'],
|
||||
transform: (rawState) => ({
|
||||
muted: rawState.mediaMuted ?? false,
|
||||
volumeLevel: rawState.mediaVolumeLevel ?? 'off'
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* MuteButton props hook - equivalent to React's useMuteButtonProps
|
||||
* Handles element attributes and properties based on state
|
||||
*/
|
||||
export const useMuteButtonProps: PropsHook<{ muted: boolean; volumeLevel: string }> = (state, element) => {
|
||||
// Handle boolean data attribute: present with empty string when true, absent when false
|
||||
// This matches the React component behavior exactly
|
||||
if (state.muted) {
|
||||
element.setAttribute('data-muted', '');
|
||||
} else {
|
||||
element.removeAttribute('data-muted');
|
||||
}
|
||||
|
||||
// Set volume level data attribute
|
||||
element.setAttribute('data-volume-level', state.volumeLevel);
|
||||
|
||||
// Set element properties for backwards compatibility
|
||||
// @ts-ignore - Custom element property
|
||||
element.mediaMuted = state.muted;
|
||||
// @ts-ignore - Custom element property
|
||||
element.mediaVolumeLevel = state.volumeLevel;
|
||||
};
|
||||
|
||||
/**
|
||||
* MuteButton events hook - equivalent to React's event handlers
|
||||
* Handles event dispatch to media store
|
||||
*/
|
||||
export const useMuteButtonEvents: EventsHook = {
|
||||
events: ['mediamuterequest', 'mediaunmuterequest'],
|
||||
handler: (event, mediaStore) => {
|
||||
if (['mediamuterequest', 'mediaunmuterequest'].includes(event.type)) {
|
||||
const { type, detail } = event;
|
||||
mediaStore.dispatch({ type, detail });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Connected MuteButton component using hook-style architecture
|
||||
* Equivalent to React's MuteButton = toConnectedComponent(...)
|
||||
*/
|
||||
export const MuteButton = toConnectedHTMLComponent(
|
||||
MediaMuteButton,
|
||||
useMuteButtonState,
|
||||
useMuteButtonProps,
|
||||
useMuteButtonEvents,
|
||||
'MuteButton'
|
||||
);
|
||||
|
||||
export default MuteButton;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { toConnectedHTMLComponent, StateHook, PropsHook, EventsHook } from '../utils/component-factory';
|
||||
import { MediaPlayButton } from './ui/media-play-button';
|
||||
|
||||
/**
|
||||
* PlayButton state hook - equivalent to React's usePlayButtonState
|
||||
* Handles media store state subscription and transformation
|
||||
*/
|
||||
export const usePlayButtonState: StateHook<{ paused: boolean }> = {
|
||||
keys: ['mediaPaused'],
|
||||
transform: (rawState) => ({
|
||||
paused: rawState.mediaPaused ?? true
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* PlayButton props hook - equivalent to React's usePlayButtonProps
|
||||
* Handles element attributes and properties based on state
|
||||
*/
|
||||
export const usePlayButtonProps: PropsHook<{ paused: boolean }> = (state, element) => {
|
||||
// Handle boolean data attribute: present with empty string when true, absent when false
|
||||
// This matches the React component behavior exactly
|
||||
if (state.paused) {
|
||||
element.setAttribute('data-paused', '');
|
||||
} else {
|
||||
element.removeAttribute('data-paused');
|
||||
}
|
||||
|
||||
// Set element property for backwards compatibility
|
||||
// @ts-ignore - Custom element property
|
||||
element.mediaPaused = state.paused;
|
||||
};
|
||||
|
||||
/**
|
||||
* PlayButton events hook - equivalent to React's event handlers
|
||||
* Handles event dispatch to media store
|
||||
*/
|
||||
export const usePlayButtonEvents: EventsHook = {
|
||||
events: ['mediaplayrequest', 'mediapauserequest'],
|
||||
handler: (event, mediaStore) => {
|
||||
if (['mediaplayrequest', 'mediapauserequest'].includes(event.type)) {
|
||||
const { type, detail } = event;
|
||||
mediaStore.dispatch({ type, detail });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Connected PlayButton component using hook-style architecture
|
||||
* Equivalent to React's PlayButton = toConnectedComponent(...)
|
||||
*/
|
||||
export const PlayButton = toConnectedHTMLComponent(
|
||||
MediaPlayButton,
|
||||
usePlayButtonState,
|
||||
usePlayButtonProps,
|
||||
usePlayButtonEvents,
|
||||
'PlayButton'
|
||||
);
|
||||
|
||||
export default PlayButton;
|
||||
@@ -1,12 +1,10 @@
|
||||
import { toConnectedMediaMuteButton } from "../connected/media-mute-button";
|
||||
import { MediaMuteButton as BaseMediaMuteButton } from "../ui/media-mute-button";
|
||||
const MediaMuteButton = toConnectedMediaMuteButton(BaseMediaMuteButton);
|
||||
import { MuteButton } from '../MuteButton.js';
|
||||
|
||||
// NOTE: In this architecture it will be important to decouple component class definitions from their registration in the CustomElementsRegistry. (CJP)
|
||||
if (!globalThis.customElements.get('media-mute-button')) {
|
||||
// @ts-ignore - Custom element constructor compatibility
|
||||
globalThis.customElements.define('media-mute-button', MediaMuteButton);
|
||||
globalThis.customElements.define('media-mute-button', MuteButton);
|
||||
}
|
||||
|
||||
export { MediaMuteButton };
|
||||
export default MediaMuteButton;
|
||||
export { MuteButton as MediaMuteButton };
|
||||
export default MuteButton;
|
||||
@@ -1,12 +1,10 @@
|
||||
import { toConnectedMediaPlayButton } from "../connected/media-play-button";
|
||||
import { MediaPlayButton as BaseMediaPlayButton } from "../ui/media-play-button";
|
||||
const MediaPlayButton = toConnectedMediaPlayButton(BaseMediaPlayButton);
|
||||
import { PlayButton } from '../PlayButton.js';
|
||||
|
||||
// NOTE: In this architecture it will be important to decouple component class definitions from their registration in the CustomElementsRegistry. (CJP)
|
||||
if (!globalThis.customElements.get('media-play-button')) {
|
||||
// @ts-ignore - Custom element constructor compatibility
|
||||
globalThis.customElements.define('media-play-button', MediaPlayButton);
|
||||
globalThis.customElements.define('media-play-button', PlayButton);
|
||||
}
|
||||
|
||||
export { MediaPlayButton };
|
||||
export default MediaPlayButton;
|
||||
export { PlayButton as MediaPlayButton };
|
||||
export default PlayButton;
|
||||
@@ -1,65 +0,0 @@
|
||||
import { ConsumerMixin } from '@open-wc/context-protocol';
|
||||
|
||||
// NOTE: This should be fairly generic code that could itself be abstracted into configuration for a more generic factory implementation. (CJP)
|
||||
export const toConnectedMediaMuteButton = (BaseClass = HTMLElement) => {
|
||||
// @ts-ignore - Custom element constructor compatibility
|
||||
return class MediaMuteButton extends ConsumerMixin(BaseClass) {
|
||||
static get observedAttributes(): string[] {
|
||||
return [
|
||||
// @ts-ignore
|
||||
...(super.observedAttributes ?? []),
|
||||
'mediapaused',
|
||||
];
|
||||
}
|
||||
|
||||
_mediaStore: any;
|
||||
|
||||
contexts = {
|
||||
mediaStore: (mediaStore: any) => {
|
||||
this._mediaStore = mediaStore;
|
||||
|
||||
this._mediaStore.subscribeKeys(
|
||||
['mediaVolumeLevel', 'mediaMuted'],
|
||||
({ mediaVolumeLevel, mediaMuted }: any) => {
|
||||
/** @ts-ignore */
|
||||
this.mediaVolumeLevel = mediaVolumeLevel;
|
||||
/** @ts-ignore */
|
||||
this.mediaMuted = mediaMuted;
|
||||
// @ts-ignore - Element property access
|
||||
this.setAttribute('data-volume-level', mediaVolumeLevel);
|
||||
// @ts-ignore - Element property access
|
||||
this.toggleAttribute('data-muted', mediaMuted);
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback?.();
|
||||
// @ts-ignore - Element property access
|
||||
this.addEventListener('mediamuterequest', this);
|
||||
// @ts-ignore - Element property access
|
||||
this.addEventListener('mediaunmuterequest', this);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback?.();
|
||||
// @ts-ignore - Element property access
|
||||
this.removeEventListener('mediamuterequest', this);
|
||||
// @ts-ignore - Element property access
|
||||
this.removeEventListener('mediaunmuterequest', this);
|
||||
}
|
||||
|
||||
handleEvent(event: CustomEvent) {
|
||||
/** @ts-ignore */
|
||||
super.handleEvent?.(event);
|
||||
if (
|
||||
this._mediaStore &&
|
||||
['mediamuterequest', 'mediaunmuterequest'].includes(event.type)
|
||||
) {
|
||||
const { type, detail } = event;
|
||||
this._mediaStore.dispatch({ type, detail });
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,63 +0,0 @@
|
||||
import { ConsumerMixin } from '@open-wc/context-protocol';
|
||||
|
||||
// NOTE: This should be fairly generic code that could itself be abstracted into configuration for a more generic factory implementation. (CJP)
|
||||
export const toConnectedMediaPlayButton = (BaseClass = HTMLElement) => {
|
||||
return class MediaPlayButton extends ConsumerMixin(BaseClass) {
|
||||
static get observedAttributes(): string[] {
|
||||
return [
|
||||
// @ts-ignore
|
||||
...(super.observedAttributes ?? []),
|
||||
'mediapaused',
|
||||
];
|
||||
}
|
||||
|
||||
_mediaStore: any;
|
||||
|
||||
contexts = {
|
||||
mediaStore: (mediaStore: any) => {
|
||||
this._mediaStore = mediaStore;
|
||||
|
||||
this._mediaStore.subscribeKeys(
|
||||
['mediaPaused'],
|
||||
({ mediaPaused }: any) => {
|
||||
// NOTE: We may want to assume setting properties instead of attributes here to leave things generic for
|
||||
// complex values. That allows implementors to decide what to do if/when the property is set.
|
||||
// Using `this.toggleAttribute('mediapaused', mediaPaused);` should also work if that's preferred.
|
||||
/** @ts-ignore */
|
||||
this.mediaPaused = mediaPaused;
|
||||
// @ts-ignore - Element property access
|
||||
this.toggleAttribute('data-paused', mediaPaused);
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback?.();
|
||||
// @ts-ignore - Element property access
|
||||
this.addEventListener('mediaplayrequest', this);
|
||||
// @ts-ignore - Element property access
|
||||
this.addEventListener('mediapauserequest', this);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback?.();
|
||||
// @ts-ignore - Element property access
|
||||
this.removeEventListener('mediaplayrequest', this);
|
||||
// @ts-ignore - Element property access
|
||||
this.removeEventListener('mediapauserequest', this);
|
||||
}
|
||||
|
||||
handleEvent(event: CustomEvent) {
|
||||
/** @ts-ignore */
|
||||
super.handleEvent?.(event);
|
||||
if (
|
||||
this._mediaStore &&
|
||||
['mediaplayrequest', 'mediapauserequest'].includes(event.type)
|
||||
) {
|
||||
const { type, detail } = event;
|
||||
this._mediaStore.dispatch({ type, detail });
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,10 @@
|
||||
export * as MediaProvider from './media-provider.js';
|
||||
export * as MediaThemeDefault from './skins/media-skin-default.js';
|
||||
|
||||
// New hook-style components
|
||||
export { PlayButton } from './components/PlayButton.js';
|
||||
export { MuteButton } from './components/MuteButton.js';
|
||||
|
||||
export function defineVjsPlayer() {
|
||||
/** @TODO - Reimplement me (at least as a POC) (CJP) */
|
||||
// defineVideoProvider();
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { ConsumerMixin } from '@open-wc/context-protocol';
|
||||
|
||||
/**
|
||||
* Generic types for HTML component hooks pattern
|
||||
* Mirrors the React hooks architecture for consistency
|
||||
*/
|
||||
export type StateHook<T = any> = {
|
||||
keys: string[];
|
||||
transform: (rawState: any) => T;
|
||||
};
|
||||
|
||||
export type PropsHook<T = any> = (state: T, element: HTMLElement) => void;
|
||||
|
||||
export type EventsHook = {
|
||||
events: string[];
|
||||
handler: (event: CustomEvent, mediaStore: any) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic factory function to create connected HTML components using hooks pattern.
|
||||
* Provides equivalent functionality to React's toConnectedComponent but for custom elements.
|
||||
*
|
||||
* @param BaseClass - Base custom element class to extend
|
||||
* @param stateHook - Hook that defines state keys and transformation logic
|
||||
* @param propsHook - Hook that handles element attributes and properties based on state
|
||||
* @param eventsHook - Hook that defines event handling logic
|
||||
* @param displayName - Display name for debugging
|
||||
* @returns Connected custom element class with media store integration
|
||||
*/
|
||||
export const toConnectedHTMLComponent = <TState = any>(
|
||||
BaseClass: CustomElementConstructor,
|
||||
stateHook: StateHook<TState>,
|
||||
propsHook: PropsHook<TState>,
|
||||
eventsHook: EventsHook,
|
||||
displayName?: string
|
||||
) => {
|
||||
const ConnectedComponent = class extends ConsumerMixin(BaseClass) {
|
||||
static get observedAttributes(): string[] {
|
||||
return [
|
||||
// @ts-ignore
|
||||
...(super.observedAttributes ?? [])
|
||||
];
|
||||
}
|
||||
|
||||
_mediaStore: any;
|
||||
_state: TState | undefined;
|
||||
|
||||
contexts = {
|
||||
mediaStore: (mediaStore: any) => {
|
||||
this._mediaStore = mediaStore;
|
||||
|
||||
// Subscribe to media store state changes
|
||||
// Split into two phases: state transformation, then props update
|
||||
this._mediaStore.subscribeKeys(
|
||||
stateHook.keys,
|
||||
(rawState: any) => {
|
||||
// Phase 1: Transform raw media store state (state concern)
|
||||
this._state = stateHook.transform(rawState);
|
||||
|
||||
// Phase 2: Update element attributes/properties (props concern)
|
||||
if (this._state !== undefined) {
|
||||
// @ts-ignore - Element property access
|
||||
propsHook(this._state, this);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback?.();
|
||||
|
||||
// Set up event listeners
|
||||
eventsHook.events.forEach(eventType => {
|
||||
// @ts-ignore - Element property access
|
||||
this.addEventListener(eventType, this);
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback?.();
|
||||
|
||||
// Clean up event listeners
|
||||
eventsHook.events.forEach(eventType => {
|
||||
// @ts-ignore - Element property access
|
||||
this.removeEventListener(eventType, this);
|
||||
});
|
||||
}
|
||||
|
||||
handleEvent(event: CustomEvent): void {
|
||||
// @ts-ignore
|
||||
super.handleEvent?.(event);
|
||||
|
||||
// Delegate to event hook if media store is available
|
||||
if (this._mediaStore) {
|
||||
eventsHook.handler(event, this._mediaStore);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Set display name for debugging and dev tools
|
||||
if (displayName) {
|
||||
Object.defineProperty(ConnectedComponent, 'name', { value: displayName });
|
||||
}
|
||||
|
||||
return ConnectedComponent;
|
||||
};
|
||||
Reference in New Issue
Block a user