-
-
Notifications
You must be signed in to change notification settings - Fork 924
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replace key:
with m.link(...)
and m.track(...)
#2691
Comments
I don't really get this. Can you do a little example on how you would do a |
I'm good with this in theory, but I'm confused about the semantic choice of |
I agree with Scotty, The crucial problem with So I reason that since we have two distinct enough signatures we can still capture intent and produce pertinent warnings in cases where the input isn’t conformant — m(Header),
[showModal && m(Modal, {key: 'modal'})],
m(PageContent)
// vs
m(Header),
m.track('modal', showModal && m(Modal),
m(PageContent) |
Edit: fix a name, add contrast @CreaturesInUnitards @barneycarroll @StephanHoyer Names are easily bikeshed. But the idea is that you could do this: // Utility component
function Async(initialProps) {
const ctrl = new AbortController()
let state = "pending"
let value
new Promise(resolve => resolve(initialProps.fetch(ctrl.signal))).then(
v => { state = "ready"; value = v },
e => { state = "error"; value = e }
)
return props => [
m.release(() => ctrl.abort()),
// Don't diff `pending` and `ready`/`error`, just replace entirely.
m.link(state,
state === "pending" ? props.pending()
: state === "ready" ? props.ready(value)
: props.error(value)
),
]
}
// This automatically re-fetches every time you change stores, and always keeps the product
// availability count up to date when you change the filter, all with zero resource leaks.
function ProductTable({storeId}) {
let filter = "all"
return () => m(".item-table",
m(".item-header", props.header),
m(".item-filter", "Item type: ", m("select",
m("option", {value: "all", selected: filter === "all"}, "All"),
m("option", {value: "shirts", selected: filter === "shirts"}, "Shirts"),
m("option", {value: "shorts", selected: filter === "shorts"}, "Shorts"),
m("option", {value: "skirts", selected: filter === "skirts"}, "Skirts"),
m("option", {value: "jackets", selected: filter === "jackets"}, "Jackets")
)),
m.link(id, m(Async, {
fetch: signal => m.request("/api/:storeId/products", {params: {storeId}, signal}),
pending: () => m(LoadingSpinner, {variant: "large"}),
error: e => m(ErrorDisplay, {error: e}),
ready: ({products}) => m("table",
m("tr",
m("th", "Name"),
m("th", "Description"),
m("th", "Available")
),
m.track(
products
.filter(product => filter === "all" || product.type === filter)
.map(product => ({
key: item.id,
value: m("tr",
m("td", m(m.route.Link, {href: product.id}, product.name),
m("td", product.shortDesc),
m("td", m(Async, {
fetch: signal => m.request("/api/:storeId/products/:id/inventory", {
params: {storeId, id: item.id},
signal,
}),
pending: () => m(LoadingSpinner, {variant: "small"}),
error: e => m(ErrorDisplay, {error: e}),
ready: inventory => inventory.available,
}))),
),
})
),
)
}))
)
} With // Utility component
function Async(initialProps) {
const ctrl = new AbortController()
let state = "pending"
let value
new Promise(resolve => resolve(initialProps.fetch(ctrl.signal))).then(
v => { state = "ready"; value = v },
e => { state = "error"; value = e }
)
return props => [
m.release(() => ctrl.abort()),
// Don't diff `pending` and `ready`/`error`, just replace entirely.
[m.fragment({key: state},
state === "pending" ? props.pending()
: state === "ready" ? props.ready(value)
: props.error(value)
)],
]
}
// This automatically re-fetches every time you change stores, and always keeps the product
// availability count up to date when you change the filter, all with zero resource leaks.
function ProductTable({storeId}) {
let filter = "all"
return () => m(".item-table",
m(".item-header", props.header),
m(".item-filter", "Item type: ", m("select",
m("option", {value: "all", selected: filter === "all"}, "All"),
m("option", {value: "shirts", selected: filter === "shirts"}, "Shirts"),
m("option", {value: "shorts", selected: filter === "shorts"}, "Shorts"),
m("option", {value: "skirts", selected: filter === "skirts"}, "Skirts"),
m("option", {value: "jackets", selected: filter === "jackets"}, "Jackets")
)),
[m(Async, {
id: storeId,
fetch: signal => m.request("/api/:storeId/products", {params: {storeId}, signal}),
pending: () => m(LoadingSpinner, {variant: "large"}),
error: e => m(ErrorDisplay, {error: e}),
ready: ({products}) => m("table",
m("tr",
m("th", "Name"),
m("th", "Description"),
m("th", "Available")
),
products
.filter(product => filter === "all" || product.type === filter)
.map(product =>
m("tr", {key: item.id},
m("td", m(m.route.Link, {href: product.id}, product.name),
m("td", product.shortDesc),
m("td", m(Async, {
fetch: signal => m.request("/api/:storeId/products/:id/inventory", {
params: {storeId, id: item.id},
signal,
}),
pending: () => m(LoadingSpinner, {variant: "small"}),
error: e => m(ErrorDisplay, {error: e}),
ready: inventory => inventory.available,
}))),
)
),
)
})]
)
} Hopefully this clarifies where I'm coming from. |
Not sure if it should be different, but I think it's worth pointing out |
@gilbert That's true, but It does raise the question of if we should kill off |
IMO, as a dev who uses Mithril and has done so for a fair while now already, I feel this increases the surface complexity of using Mithril enough to be a hard sell. I understand that |
I'll go ahead and close this for now. I'm not spending the energy to pursue this - the workaround does technically work, and there isn't really much to be gained here. |
Mithril version:
Platform and OS:
Project:
Is this something you're interested in implementing yourself?
Description
Replace the
key:
attribute with two helpers:m.link(key, ...children)
to replace the single-child keyed fragment idiomm.track([{key, value}, ...])
to replace collections of keyed elementsThe
tag
ofm.link(...)
would be"$"
, and thetag
ofm.track(...)
would be"@"
.Related: #2690, #2689, #2689
Why
Couple reasons:
return input.map(Vnode.normalize)
there and it magically works.Possible Implementation
This is of course the complicated part and would require a lot of code movement. TL;DR:
vnode.key
goes away, and instead I'd usevnode.state
to hold the key form.link(...)
and key list form.track(...)
.m.track(...)
to separate key and child lists, this would mean we can still keepvnode.children
set to a raw array and not have to specially handle it in the removal logic.switch
statement for the vnode tag"@"
.vnode.tag === "$"
increateNode
andupdateNode
as applicable, largely delegating to what's used by fragments..key
-related could just die.Open Questions
m.track
- is that okay, or should it be changed to an array or something else? An object will slightly grow the size of most multiline keyed fragments in lines of code, but not significantly in minified size, and an array would be slightly smaller but less immediately clear just from reading it..bind
already exists - it'sFunction.prototype.bind
, and I really don't want to go down that rabbit hole (you'll inevitably run people into really arcane edge cases fast, and even Babel historically relied on it for some things). I've consideredm.collect
before going withm.track
, but yeah, I'm not fond of either to be quite honest.The text was updated successfully, but these errors were encountered: