Open Enyium opened 2 months ago
There is many usecases:
We need to think of a syntax for different things.
for X in @children : SomeComponent { X{} }
@children[0]
for the first children. How to make children mandatory or optional?@children(foobar)
and then Foo { foobar |= Rectangle { ... } }
(not using quotes here because it should be identifiers)(All my proposed syntax there are just some quick example, but nicer syntax need to be sorted out.)
@children["body"]
that look nice :-)
We need to think of a syntax for different things.
- Loop through children. (eg
for X in @children : SomeComponent { X{} }
- Maybe
@children[0]
for the first children. How to make children mandatory or optional?
This actually involves batches of children. The current @children
would directly map to the new @children["default"]
/@children(default)
. Each slot could be filled with multiple siblings of children, as you see in my code above, just like this is now the case with the one and only slot possible @children
. You should also be able to specify multiple non-Slot
elements with slot
properties in succession using the same name, which would be identical to provide a Slot
with multiple children.
I'm not sure how iterating over children would fit into this proposal. Different slot contents should be able to be placed anywhere in the internal tree of a component, even in a different order than at the usage site. If it would ever be useful to use a for
loop with children, I think this would be separate issue.
I admit index access like @children[1]
meaning a batch of children could be confusing. Maybe named slots would be enough.
With Vue.js, you can even pass slot-specific data out to the slot usage site. See here. I currently don't know what could possibly be the cases where one would need this with Slint, however.
If you'd want to implement this also, at the component usage site, this could look like:
Slot {
name: "footer";
// Or simply `in property` without `slot`.
slot in property <int> name-from-component-but-unsuitable-because-already-in-use as received;
Text { text: "The Footer\n\{received}"; }
}
In the component, it could perhaps look like the following:
@children(footer) {
out property <int> the-name: 123;
}
Maybe, out
in the component and in
at the usage site would be the only variation that'd make sense, so everything else should be disallowed?
I personally have to wonder if children shouldn't be properties to begin with ā then you could explicitly communicate if it supports multiple, one, or if they're optional. Or even override default children implementations. It would be largely the same as assigning named elements is now as well.
One further benefit would be that accessing them would be available outside of slint, using the same syntax that other properties already use. Meaning it could be a bit easier to do dynamic views, like a stack based UI for example, without needing models.
Though perhaps you have good reasons for disallowing all of this ā it just seems to me like a nice fit. :slightly_smiling_face:
accessing them would be available outside of slint, using the same syntax that other properties already use.
This reminds me of #2390. But I also read something about them not wanting to provide access to children, because not doing so gives them more freedom to optimize the Slint code.
Aye, I imagine there's good arguments against doing it, and I was unaware of that particular issue - but indeed that's precisely what I was talking about. It's a use case we're going to have eventually, as we're planning to use interpreted slint in our project and let users manipulate the UI in a plug-n-play sort of fashion (hopefully). š
If the children were exposed as a model or array at runtime, that would be cool. But what should the ownership be? Is a property a strong reference? (Risk of leaks through circular references) Is it a weak reference? (What happens if the object is deleted during binding evaluation)
I think if we can find good answers then that might level us up :)
I would see things staying mostly as they are now, and user-defined properties can't be element ref
. This nips any issues with circular references in the bud, I think?
Though you bring up very good points, and I'm left wondering, how does focus-scope
avoid these issues right now? Is it simply impossible to create circular references due to the current constraints?
My proposal would be that users can only define properties which own the underlying element, and are neither strong nor weak references. These owning properties will always be Copy
/Clone
/Default
constructed. Though they very well could be strong references under the hood, but non-internal code would be disallowed from constructing them as such.
Thus allowing the following:
component Page {
// in lieu of optionals, these would be default constructed
// and conditional expansion would be used, based on other properties
in-out property <PageHeader> header;
in-out property <PageBody> body;
in-out property <[PageButton]> buttons;
// using the same syntax for expansion
VerticalLayout {
@header
@body
HorizontalLayout {
// could be simplified as `@buttons`
for button in buttons {
@button
}
}
}
}
component HomePage inherits Page {
header := PageHeader {
text: "Hello, World!";
}
body := PageBody {
text: "Lorem ipsum dolor sit amet"
}
buttons := [
PageButton { text: "1"; },
PageButton { text: "2"; },
PageButton { text: "3"; },
]
}
I would then propose, that children
becomes an explicit property on SlintElement
:
struct SlintElement {
in-out property <[SlintElement]> children;
}
The current "magic" of coalescing all children into that array is kept, as is the current @
syntax for expansion. It might be nice to also support a less verbose syntax for assigning arrays of elements, though I don't see that as being strictly required.
Though these are just my off the cuff thoughts, and perhaps there are still issues left unresolved? My apologies if this is derailing this thread by the way - happy to move this discussion elsewhere if you'd like. š
@SK83RJOSH I think that could be solved with ComponentContainer/component-factory and interfaces. Some discussion about that in https://github.com/slint-ui/slint/issues/2390
I've seen this offered elsewhere with the following syntax:
// MyMagicButton.zing
Item {
alias icon: innerIcon;
alias moreIcons: moreInnerIcons;
innerIcon := Item {
}
moreInnerIcons := Column {
}
}
// Using the component
MyMagicButton {
icons {
Image { source: "../myicon.png"};
}
moreIcons {
Image { source: "../myicon.png"};
}
}
Where I think alias
is conceptually similar to <=>
but at least for me clearer. "Expose this internal property/callback directly, but with a new name".
This would make adding say an icon to a button easy. However in Zing this concept is aided by visual items having a 'children' property. This is a read only array of children the item has. Something that has many uses! But in this case it can be used to allow items to have a default implementation that is not used if the component has some children added.
export component ExportedComponent {
alias icons: headerIcons;
headerIcons := VerticalLayout {}
if headerIcons.children.length == 0 : Rectangle {
... default icon or whatever
}
}
I saw talk about being able to type or enforce interfaces which sounds next level amazing. To think of a future where we have some kind of Navigation components that will allow nice transitions between different views. But able to enforce each component someone adds actually is a View or whatever sounds so useful.
As someone who comes from Angular
, I would like to chime in on another type of syntax that is partially similar to angular.
Here we create three components for the example: Header
, Footer
, and Paragraph
.
export component Header {
in property: text;
Text { text: text; }
}
export component Footer {
in property: text;
Text { text: text; }
}
export component Paragraph {
in property: text;
Text { text: text; }
}
We then use another component that renders the three items where children (when used with a parameter) would receive a component type as an argument (or even a list of components). When no arguments are passed, all the remaining components that didn't match a selector, would render to that slot.
export component Main {
HorizontalLayout {
// Can only have one
@children(Header);
}
VerticalLayout {
// Can only have one
@children
}
HorizontalLayout {
// Can only have one
@children(Footer);
}
}
Now the last thing to do is create the main component with its content maybe something like this:
export component App inherits Window {
Main {
Header { text: "Welcome"; }
Footer { text: "Goodbye"; }
Paragraph { text: "Lorem ipsum odor amet, consectetuer adipiscing elit." }
Paragraph { text: "Lorem ipsum dolor sit amet." }
Button { text: "Hello World"; }
Footer { text: "See you next time!"; }
}
}
The rendered output would look like this. Note that the two footers were originally out of order, and now they are grouped together:
export component Main {
HorizontalLayout {
Header { text: "Welcome"; }
}
VerticalLayout {
Paragraph { text: "Lorem ipsum odor amet, consectetuer adipiscing elit." }
Paragraph { text: "Lorem ipsum dolor sit amet." }
Button { text: "Hello World"; }
}
HorizontalLayout {
Footer { text: "Goodbye"; }
Footer { text: "See you next time!"; }
}
}
Now angular also has reusable templates that you can reuse since you can only use @children(xxx)
one time, however the template can be reused:
export component Main {
HorizontalLayout {
@template(MyTemplate);
}
VerticalLayout {
@template(MyTemplate);
}
HorizontalLayout {
@template(MyTemplate);
}
template MyTemplate {
HorizontalLayout {
@children(MyCompnent);
}
}
}
Vue.js has a slot concept.
The following code imagines how this concept could manifest in Slint:
This feature would allow for better abstractions.
This SlintPad demo shows the use case that made me desire the feature. Change the size of the preview to see what it's about. In my app, there are control buttons that must always be centered, no matter what's displayed to their left or right. To the left of the buttons, there may be informational
Text
s, right-aligned in their block (so, directly next to the buttons). The space to the right of the buttons is used to show error messages, if needed.