My previous post on hearsay discusses how this problem came up for me.
In hearsay, I want to be able to pass data into components using the props
attribute, and specifying a JavaScript object literal. Something like this:
<hear-say src="Comp.html" props="{ n: Math.random() }"></hear-say>
Let's start with the fundamental task: the attribute should be interpreted as JavaScript code. So I created a custom getter:
get props()
{
const prop_att = this.getAttribute("props")?.trim() || "{}";
const prop_func = Function("self", `return ${prop_att};`);
return prop_func(this);
}
This creates a new function with a single argument self
, and the body of the function simply returns whatever the expression evaluates to. So every time this component accesses self.props.n
, it will call Math.random() and return a new value.
This is good, but a straightforward setter turned out to be a problem.
set props(val)
{
this.setAttribute("props", JSON.stringify(val));
}
When I tried updating the value of props
, I ran into a problem. A line of code like self.props = { ...self.props }
(never mind accomplishing anything useful) would result in the value changing from { n: Math.random() }
to, e.g., { n: 0.42 }
. The code would be overwritten with a value.
It occurred to me that I could create another property, call it propsData
, that could hold data written to props
. This way you can write whatever you want to props
, and it won't overwrite the code there.
I changed the setter to store data in propsData
. And it places the data into a props-data
attribute to make it easy to see when inspecting the DOM.
set props(val)
{
this.propsData = val;
this.setAttribute("props-data", JSON.stringify(val));
}
It's a little weird, the behavior isn't immediately intuitive I'm afraid, but on balance I think it's a good approach. Now I just had to give myself more work.
I thought it would be nice if the user could update props
(or propsData
, really) by mutating it. The solution that came to mind was to create a special Proxy for props
that also incorporates propsData
.
What I want it to do is sort of synthesize props
and propsData
. When code references props.foo
, I want it to check both props
and propsData
for foo
. If it exists in both places, it will prefer the propsData
version. If foo
doesn't exist in propsData
, it will return whatever props.foo
holds (if anything).
So far so good. But what if props
holds nested objects? Code that sets, e.g., props.foo.bar.baz = 42
will run into an issue. As it goes down the props
chain (getting first foo
and then bar
) the corresponding chain under propsData
is empty (there is no propsData.foo
, much less propsData.foo.bar
). To compensate, if the getter finds an object property in the props
chain and no corresponding propsData
property, it will fill it in with an empty object.
This is basically how the current version of the code works, with one more provision. When evaluating an expression like props.foo.bar = 42
, foo
will return an object wrapped in a Proxy, which basically holds two things: the value for foo
inside props
, and inside propsData
. This is exactly what it does for the props
chain, but the propsData
chain needs to be a little different.
At first I had it construct an object with one field for the props
chain and one for the propsData
chain. The Proxy target for foo
in props.foo
, for example, would look like { props: {bar: 42}, propsData: {} }
. But that leaves the propsData
chain disconnected from propsData
. Instead, the Proxy target changed: a prop
property was added. Now instead of the propsData
property holding the value of, e.g., foo.bar
, it would hold the value of foo
, and prop
would be bar
.
This setup allows a backwards chaining effect. The Proxy for bar
would look like { props: 42, propsData: propsData.foo, prop: "bar" }
. The Proxy's setter runs target.propsData[target.prop] = val
. target.propsData
will be propsData.foo
, so the effect is the same as setting propsData.foo.bar = val
.
I hope you found this post somewhat useful. I fear this explanation is not as clear as I had hoped. If you have any questions, please feel free to comment.
Top comments (0)