S
SolidJS•7mo ago
apademide

useContext() from custom element

I'm trying to access a context from a web component. It's a single global context holding a singleton class as the value which provides access to the logged in user. It works as expected, but I can't find a way to make it accessible from web components. I use web components to pass template strings as html from a JSON API; the aim here with wk-user is to be able to achieve syntax such as Hello, <wk-user property="name"></wk-user>! rather than creating a whole templating system. I tried using solid-element's customElement:
customElement("wk-user", {}, () => {
const User = useUser();

console.log(User);

return <div>{JSON.stringify(User)}</div>;
});
customElement("wk-user", {}, () => {
const User = useUser();

console.log(User);

return <div>{JSON.stringify(User)}</div>;
});
and component-register's compose + register + solid-element's withSolid
compose(
register("wk-user", {}),
withSolid,
)(() => {
const User = useUser();

console.log(User);

return <div>{JSON.stringify(User)}</div>;
});
compose(
register("wk-user", {}),
withSolid,
)(() => {
const User = useUser();

console.log(User);

return <div>{JSON.stringify(User)}</div>;
});
where both cases where undefined I've also tried messing around with getOwner(), but getOwner()'s context prop appears to always be null within web components Any help or information appreciated as I don't know where to even continue investigating
10 Replies
apademide
apademide•7mo ago
Additional context With the implementation of the User class trimmed, this is the context definition
import { createContext, useContext, type JSX } from "solid-js";

type TUserProviderProps = {
children: JSX.Element;
};

const UserContext = createContext<ReturnType<typeof createUser>>();

export function UserProvider(props: TUserProviderProps) {
return (
<UserContext.Provider value={createUser(props)}>
{props.children}
</UserContext.Provider>
);
}

export function useUser() {
return useContext(UserContext);
}

function createUser(props) {
class User {

}

return User;
}
import { createContext, useContext, type JSX } from "solid-js";

type TUserProviderProps = {
children: JSX.Element;
};

const UserContext = createContext<ReturnType<typeof createUser>>();

export function UserProvider(props: TUserProviderProps) {
return (
<UserContext.Provider value={createUser(props)}>
{props.children}
</UserContext.Provider>
);
}

export function useUser() {
return useContext(UserContext);
}

function createUser(props) {
class User {

}

return User;
}
Also, worth mentionning the User context depends on another context (defined the same way):
function createUser() {
const Navigation = useNavigation();

class User {
// A few functions using Navigation
}

return User;
}
function createUser() {
const Navigation = useNavigation();

class User {
// A few functions using Navigation
}

return User;
}
with this at the top level of the app's tree
<NavigationProvider>
<UserProvider>
{props.children}
</UserProvider>
</NavigationProvider>;
<NavigationProvider>
<UserProvider>
{props.children}
</UserProvider>
</NavigationProvider>;
bigmistqke 🌈
bigmistqke 🌈•7mo ago
mm, it interestingly enough works if the context-provider is also a webcomponent https://playground.solidjs.com/anonymous/55d0ff7b-114d-45d2-a793-d9791f500bef
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
bigmistqke 🌈
bigmistqke 🌈•7mo ago
I would issue it as a bug
apademide
apademide•7mo ago
Ok I could setup a minimal example of the issue in both cases Will open an issue but include it here for ref too
import { render } from "solid-js/web";
import { createContext, useContext } from "solid-js";
import { customElement } from "solid-element";

const context = createContext(false);

customElement("my-component", {}, (props, { element }) => {
const ctx = useContext(context);
console.log("ctx", ctx);
return <div>Can access context: {ctx ? "yes" : "no"}</div>
});

customElement("my-provider", {}, (props, { element }) => {
return (
<context.Provider value={true}>
<slot />
</context.Provider>
);
});

function MyProvider(props: any) {
return (
<context.Provider value={true}>
{props.children}
</context.Provider>
)
}



render(
() => (<div>
<div>
Variant with a web component-based context
<div>
<my-provider>
<div innerHTML="<my-component />" />
</my-provider>
</div>
</div>

<hr />

<div>
Variant with a default Solid context
<MyProvider>
<div innerHTML="<my-component />" />
</MyProvider>
</div>

<hr />

<div>
Please note that using the web component in the JSX directly does work …
<MyProvider>
<my-component />
</MyProvider>
… with both methods
<my-provider>
<my-component />
</my-provider>
</div>
</div>),
document.getElementById("app")!,
);
import { render } from "solid-js/web";
import { createContext, useContext } from "solid-js";
import { customElement } from "solid-element";

const context = createContext(false);

customElement("my-component", {}, (props, { element }) => {
const ctx = useContext(context);
console.log("ctx", ctx);
return <div>Can access context: {ctx ? "yes" : "no"}</div>
});

customElement("my-provider", {}, (props, { element }) => {
return (
<context.Provider value={true}>
<slot />
</context.Provider>
);
});

function MyProvider(props: any) {
return (
<context.Provider value={true}>
{props.children}
</context.Provider>
)
}



render(
() => (<div>
<div>
Variant with a web component-based context
<div>
<my-provider>
<div innerHTML="<my-component />" />
</my-provider>
</div>
</div>

<hr />

<div>
Variant with a default Solid context
<MyProvider>
<div innerHTML="<my-component />" />
</MyProvider>
</div>

<hr />

<div>
Please note that using the web component in the JSX directly does work …
<MyProvider>
<my-component />
</MyProvider>
… with both methods
<my-provider>
<my-component />
</my-provider>
</div>
</div>),
document.getElementById("app")!,
);
bigmistqke 🌈
bigmistqke 🌈•7mo ago
nice repro interesting that
<my-provider>
<div innerHTML="<my-component />" />
</my-provider>
<my-provider>
<div innerHTML="<my-component />" />
</my-provider>
does work. lmk if u got an answer back! i think it's a rly cool idea for templating
apademide
apademide•7mo ago
apademide
apademide•7mo ago
So the answer is somewhat disappointing but Ryan provided a workaround that did the trick You may find what I came around with here: https://github.com/solidjs/solid/issues/1976#issuecomment-1846801576
GitHub
Contexts not accessible from web components created by HTML strings...
Describe the bug If required, more context is available in Solid's Discord: https://discord.com/channels/722131463138705510/1182006344128151693/1182006344128151693 General flow I have an API th...
bigmistqke 🌈
bigmistqke 🌈•7mo ago
Thanks for the update!
apademide
apademide•7mo ago
For the final solution I came with something that works quite well I made that wrapper around the compose + register methods (which passes down all logic to the default function, so it has the same features + automatically injects withSolid for convenience + pushes tags to registeredComponents)
import {
register as componentRegister,
compose,
type ComponentType,
type RegisterOptions,
type PropsDefinitionInput,
} from "component-register";
import { withSolid } from "solid-element";

type TRegister = <T>(
tag: string,
props: PropsDefinitionInput<T>,
options?: RegisterOptions & {
with?: ((C: ComponentType<any>) => ComponentType<any>)[];
},
) => (ComponentType: ComponentType<T>) => any;

const registeredComponents: string[] = [];

const register: TRegister = (tag, props, options) => {
if (!tag || !tag.includes("-")) {
throw new TypeError(
`Web component tag "${tag}" is invalid. It must be a string containing a dash.`,
);
}

if (!registeredComponents.includes(tag)) {
registeredComponents.push(tag);
} else if (process.env.NODE_ENV !== "production") {
console.warn(`Web component "${tag}" has been registered multiple times.`);
}

return compose(
componentRegister(tag, props, options),
...(() => {
if (!options?.with) return [withSolid];
if (options.with.includes(withSolid)) return options.with;
return [withSolid, ...options.with];
})(),
);
};

export { register, registeredComponents };
import {
register as componentRegister,
compose,
type ComponentType,
type RegisterOptions,
type PropsDefinitionInput,
} from "component-register";
import { withSolid } from "solid-element";

type TRegister = <T>(
tag: string,
props: PropsDefinitionInput<T>,
options?: RegisterOptions & {
with?: ((C: ComponentType<any>) => ComponentType<any>)[];
},
) => (ComponentType: ComponentType<T>) => any;

const registeredComponents: string[] = [];

const register: TRegister = (tag, props, options) => {
if (!tag || !tag.includes("-")) {
throw new TypeError(
`Web component tag "${tag}" is invalid. It must be a string containing a dash.`,
);
}

if (!registeredComponents.includes(tag)) {
registeredComponents.push(tag);
} else if (process.env.NODE_ENV !== "production") {
console.warn(`Web component "${tag}" has been registered multiple times.`);
}

return compose(
componentRegister(tag, props, options),
...(() => {
if (!options?.with) return [withSolid];
if (options.with.includes(withSolid)) return options.with;
return [withSolid, ...options.with];
})(),
);
};

export { register, registeredComponents };
Which is then used this way in my <SafeHtml> comp
if (fragment && registeredComponents.length) {
const registeredComponentsSelector = registeredComponents.join(",");
const owner = getOwner();
const customElements = fragment.querySelectorAll(
registeredComponentsSelector,
);

for (const customElement of customElements) {
(customElement as any)._$owner = owner;
}
}
if (fragment && registeredComponents.length) {
const registeredComponentsSelector = registeredComponents.join(",");
const owner = getOwner();
const customElements = fragment.querySelectorAll(
registeredComponentsSelector,
);

for (const customElement of customElements) {
(customElement as any)._$owner = owner;
}
}
I also took the opportunity to slightly improve the types inference and provide the same options as component-register's compose method with the simplicity of use of the register method
type MyProps = {
props: string
}

register("my-component",
{
props: "definition"
} as MyProps,
{
// Option object that is passed directly to component-register's register function
// so it has the same options, with the addition of the `with` option:
with: [
// List of functions that would have been passed to compose() otherwise
// withSolid included by default
(comp) => comp
]
})((props, { element }) => {
// `props` inferred as MyProps automatically

element.addPropertyChangedCallback(() => { })
element.addReleaseCallback(() => { })
element.lookupProp("foo")
element.props
element.renderRoot

return (
<div>
My Component
</div>
);
});
type MyProps = {
props: string
}

register("my-component",
{
props: "definition"
} as MyProps,
{
// Option object that is passed directly to component-register's register function
// so it has the same options, with the addition of the `with` option:
with: [
// List of functions that would have been passed to compose() otherwise
// withSolid included by default
(comp) => comp
]
})((props, { element }) => {
// `props` inferred as MyProps automatically

element.addPropertyChangedCallback(() => { })
element.addReleaseCallback(() => { })
element.lookupProp("foo")
element.props
element.renderRoot

return (
<div>
My Component
</div>
);
});
bigmistqke 🌈
bigmistqke 🌈•7mo ago
nice!