Nick
03/24/2023, 7:34 PMTextMetrics before because Label (which is the view it produces) used to need one. I'll be fixing this w/ the next release. The docs will simply import code that has been compiled so they don't get out of sync.
Can you point to the code where you're getting the error? The context is used by specific types of visualizers to provide additional info about the item being visualized. TextVisualizer shouldn't need one since it can be used in any context.Stan
03/24/2023, 10:17 PMStan
03/24/2023, 10:18 PMStan
03/24/2023, 10:20 PMStan
03/24/2023, 10:20 PMStan
03/24/2023, 10:46 PMNick
03/25/2023, 1:55 AMBasicTabbedPanelBehavior by installing basicTabbedPanelBehavior as a module. This will give you the same look/feel as you see in the docs. You'll need to make sure you use a Theme and the ThemeManager or directly instantiate BasicTabbedPanelBehavior and install it (instead of your custom behavior) to make this work. The only need to roll your own if you’d like to change the way the tabs are rendered etc. Fkr that, a look at the impl is a good place to start. Just be aware that this is a fairly advanced behavior that is given a lot of control over the panel to allow a wide range of look/feel.
Then, your TabbedPanelView has the tabbed panel as a child, but it has no proper layout, so the child has no size. You can move the size up to the TabbedPanelView and set a layout that fills it with the panel like this:
size = Size(500, 300)
children += panel
layout = constrain(panel, fill)
Finally, TextVisualizer has a version of invoke which does not take a context. Make sure you import the right one so you can simply do: textVisualizer(mapping[item] ?: "Unknown").
In terms of your question about using StyledText for the visualizer. You can do this easily by implementing a version based on how the String impl works. I'll add this to the library by the way, since it's a good suggestion.Stan
03/25/2023, 2:30 PMinstall basicTabbedPanelBehavior as a module? I'm new to kotlin, and interpret "installing a module" as importing a dependency via gradle .Nick
03/25/2023, 4:02 PMapplication method. You can see how this works for themes here.Nick
03/25/2023, 4:08 PMStan
03/25/2023, 10:04 PMStan
03/25/2023, 10:04 PMNick
03/25/2023, 10:29 PMView from that visualizer will be scaled to fit the panel. The docs show using a ScrollPanelVisualizer since the panel in that example is displaying `View`s. That visualizer will create a ScrollPanel that houses the tab’s view. Your view will just need a size so the scroll panel has something to show.Stan
03/26/2023, 6:24 PMThanks for all the help.
I do want to use the TabbedPanel as my app menu, with its "main" TabView declaring a nested "content"
container holding a LeftView, CenterView, RightView, and a FooterView.
The tabbed panel is working pretty well, and resizing itself as I resize the browser window.
The main tab view's nested container views (left, center, right, footer) also render properly (size-wise) when I refresh
the browser, but the nested views do not re-render as I resize the browser window.
Is this problem a limitation of "BasicTabbedPanelBehavior"? Is it possible to get the nested views to re-size themselves?Stan
03/26/2023, 6:24 PMHere is a pic of the layout, after browser refresh:Stan
03/26/2023, 6:25 PMHere is a pic after I re-size browser. The nested views are not re-sizing themselves.Stan
03/26/2023, 6:25 PM```
The "main" tab view, declaring the nested views: left, center, right: <https://github.com/ghubstan/doodle/blob/gnatty-app-a/Natty/src/commonMain/kotlin/io/dongxi/natty/tabbedpanel/TabView.kt>
```Can I achieve what I'm trying to do inside the nested tabbed panel view(s)?
Did I say "thanks"?Nick
03/26/2023, 6:51 PMScrollPanelVisualizer to represent the Views in each tab. This makes sense if each tab has content you want to scroll. But it sounds like you want these tabs to fill the panel and scale with it as it resizes. The good news is that the behavior will actually already do this. It's just that it is doing that with the ScrollPanel it is wrapping around each view. You can easily fix this by not using that visualizer. There is actually a ViewVisualizer which simply uses the view directly instead of wrapping it in a ScrollPanel. Try using that here instead.Nick
03/26/2023, 6:54 PMStan
03/26/2023, 9:33 PMStan
03/26/2023, 9:33 PMprivate val tabbedPanel = TabbedPanel.invoke(
<http://BoxOrientation.Top|BoxOrientation.Top>,
styledTextTabVisualizer,
homeView,
ringsView,
necklacesView,
scapularsView,
braceletsView,
earRingsView,
aboutView
).apply {
size = Size(500, 300)
Resizer(this).apply { movable = false }
}Stan
03/26/2023, 9:33 PMStan
03/26/2023, 9:33 PMStan
03/26/2023, 9:33 PMprivate val tabbedPanel = TabbedPanel(
ScrollPanelVisualizer(),
styledTextTabVisualizer,
homeView,
ringsView,
necklacesView,
scapularsView,
braceletsView,
earRingsView,
aboutView
).apply {
size = Size(500, 300)
Resizer(this).apply { movable = false }
}Stan
03/26/2023, 9:36 PMScrollPanelVisualizer() argument with a ViewVisualizer(). ViewVisualizer is an object in ItemVisualizer.kt, and ScrollPanelVisualizer is a class in ItemVisualizer.kt. I pretty sure the problem is my kotlin noobiness. Do you know how I pass a ViewVisualizer in the TabbedPanel contructor?Nick
03/26/2023, 9:39 PMStan
03/26/2023, 9:39 PMStan
03/26/2023, 9:40 PMViewVisualizer param passed, but nested views still don't resize either.Nick
03/26/2023, 9:47 PMStan
03/26/2023, 9:49 PMNick
03/27/2023, 3:12 PMthis instead of the child (you should never constrain this by the way). Also, it is redundant to set `centerX`/`centerY` once you've constrained a child's edges to match its parent's. The next issue is that you're constraining your view's using display.width etc. This isn't the right way since constraint will effectively capture that value as a constant and won't update when it changes. This is why you need to use parent in your constraints (or one of the constrained view bounds). In your case, this is actually what you want. You want to constrain based on the parent in every place where you currently use display.Stan
03/27/2023, 3:45 PMStan
03/27/2023, 3:58 PMapply {}, should I be using
size = Size(parent.width / 3, parent.height - 105)
instead of
size = Size(display!!.width / 3, display!!.height - 105)Nick
03/27/2023, 7:10 PMStan
03/27/2023, 9:00 PMStan
03/27/2023, 9:00 PMStan
03/27/2023, 9:14 PMStan
03/27/2023, 9:48 PMStan
03/27/2023, 9:48 PMNick
03/28/2023, 12:49 AMStan
03/28/2023, 10:40 PMorg.gradle.parallel = true to gradle.properties.Stan
03/28/2023, 10:48 PMStan
03/28/2023, 10:59 PMTabStrip that triggers a drop-down (menu) might be one way....Nick
03/29/2023, 1:22 AMStan
03/29/2023, 8:30 AMSNAPSHOT releases. I can only see the master branch. The master branch has a PopupMenu. Could that work too? Is PopupManager distinctly different than PopupMenu?Nick
03/29/2023, 2:03 PMNick
03/29/2023, 2:11 PMStan
03/29/2023, 5:58 PMStan
03/29/2023, 5:58 PMStan
03/29/2023, 5:59 PMNick
03/29/2023, 7:08 PMsimpleTextButtonRenderer(textMetrics) { button, canvas ->
when {
button.model.pointerOver -> canvas.rect(bounds.atOrigin, color = Color.Cyan)
else -> {
!!!! Draw a filled rect instead of a line here !!!!
canvas.line(Point(0.0, 0.5), Point(width, 0.5), stroke = Stroke(Color.Lightgray))
}
}
canvas.text(button.text, at = textPosition(button, button.text), fill = Color.Black.paint, font = font)
}
Also, you have this button accepting themes, which could override the behavior your installed. So you should set acceptsThemes to false when using inline behaviors just to be sure.Stan
03/31/2023, 3:29 PMPhotos tutorial, you load all (2) images in the application's `appScope`:
appScope.launch {
listOf("tetons.jpg", "earth.jpg").forEachIndexed { index, file ->
images.load(file)?.let { FixedAspectPhoto(it) }?.let {
import(it, display.center + Point(y = index * 50.0))
}
}
}
and instantiate FixedAspectPhoto views.
I need to lazy load images in a View. I'm calling this async function in a container's init {}:
fun loadImageInMainScope() = mainScope.launch {
image = images.load(imageSource)
println("Image: ${image.toString()}")
fixedAspectPhotoView = image?.let { FixedAspectPhoto(it) }
println("FixedAspectPhoto: ${fixedAspectPhotoView.toString()}")
photo = image?.let {
Photo(it).apply {
size = Size(100, 200)
}
}
println("photo: ${photo.toString()}")
}
(I know there is redundant stuff going on; just trying to learn how it works from your tutorial and docs...)
I cannot add photo to my container's children ( += photo!! ), then layout = contstrain(photo, fill) because photo is still null -- not loaded yet.
Briefly, how would you do this?
I point you to my dog's breakfast (code) until I get feedback and clean it up.
Thanks.Nick
03/31/2023, 3:35 PMclass LazyPhoto(pendingImage: Deferred<Image>): View() {
lateinit var image: Image
init {
pendingImage.callOnCompleted {
image = it
rerender()
}
}
override fun render(canvas: Canvas) {
if (!::image.isInitialized) return // or render a place holder
canvas.image(image)
}
}
Then you could instantiate these directly using something like:
val lazyImage = LazyImage(mainScope.async { images.load("foo.jpg")!! })Stan
03/31/2023, 3:38 PMStan
03/31/2023, 3:38 PMfun loadImageInMainScope() = mainScope.launch { ... }Stan
03/31/2023, 3:38 PMStan
03/31/2023, 3:39 PMNick
03/31/2023, 3:41 PM@OptIn(ExperimentalCoroutinesApi::class)
fun Deferred<Image>.callOnCompleted(block: (Image) -> Unit) {
job.invokeOnCompletion {
if (it == null) {
block(this.getCompleted())
}
}
}
invokeOnCompletion is still experimental in the Coroutines lib.Stan
03/31/2023, 7:17 PMNick
03/31/2023, 7:40 PMStan
03/31/2023, 8:25 PMStan
03/31/2023, 8:25 PMStan
03/31/2023, 8:26 PMNick
04/01/2023, 1:41 AMStan
04/01/2023, 5:39 PMStan
04/01/2023, 5:42 PMStan
04/01/2023, 5:44 PMNick
04/01/2023, 6:19 PMLazyPhotoView should not expose its image. It should just render it when it loads. That way you can treat it like an image view.
The docs show how to asynchrony add data to a DynamicList’s model.
launch {
listOf(
"United Kingdom" to "images/197374.svg",
"United States" to "images/197484.svg",
"France" to "images/197560.svg",
"Germany" to "images/197571.svg",
"Spain" to "images/197593.svg",
// ...
).
sortedBy { it.first }.map { (name, path) ->
imageLoader.load(path)?.let { image ->
model.add(Country(name, image))
}
}
}
This data could come from the server or wherever. Adding a new entry to the model will cause the list to update.
In your case you could create a new ring as you get data from the server and add it to the model. Each ring could be created with a lazy image that will self render when resolved. A ring visualizer could follow this pattern:
class RingView(ring: Ring): View() {
fun update(ring: Ring, index: Int, selected: Boolean) {}
}
class RingVisualizer(): ItemVisualizer<Ring, IndexedItem> {
override fun invoke(item: Ring, previous: View?, context: IndexedItem): View = when (previous) {
is RingView -> previous.apply { update(ring = item, index = context.index, selected = context.selected) }
else -> RingView(ring = item, index = context.index, selected = context.selected)
}
}Nick
04/01/2023, 7:39 PMStan
04/01/2023, 10:20 PMStan
04/01/2023, 10:21 PMStan
04/01/2023, 10:21 PMStan
04/01/2023, 10:26 PMNick
04/01/2023, 11:15 PMSmallRingView when it gets recycled. The method does nothing right now. But it should reconfigure the view to represent the new ring installed into it.
Visualizers are designed to recycle views for just this case (“infinite” list of items). My example from before should have included commentary to this effect. But essentially, you’re doing the right thing in updating and returning a SmallRingView that is already in the list (previous)—though no longer visible. It’s just that the update call leaves it in the current state, so it doesn’t render the item it is expected to.
List, and other containers that use visualizers let you benefit from this recycle behavior. This is the recommended way to implement your visualizer. But (and this isn’t recommended b/c it won’t scale) you could also ignore previous and always return a new SmallRingView. You might even try that just to get things going before optimizing by reusing the previous view.
Let me know if this works.Stan
04/02/2023, 1:58 PMStan
04/02/2023, 2:26 PMStan
04/02/2023, 2:29 PMClickedRingRecognizer object to pass selected event.source.image info to the other two sibling containers? The three sibling containers, and the ClickedRingRecognizer are defined in this class.Nick
04/02/2023, 7:43 PMSmallRingView whenever update is called.
You’ll get sets of added/removed indexes if you use the first two approaches. You can convert these to `SmallRing`s using list[index]. From there you can decide how to change state based on what was selected and deselected.
Generally your items in the list should operate independently and just update their state based on whether they are selected or not. So you might see if you can achieve the desired behavior using approach 3.
Let me know if this is helpful. Otherwise I might need to understand your use case a bit more to help.Stan
04/03/2023, 8:28 PMpendingImage in LazyPhotoView based on what is selected in DynamicList<SmallRing>.
The parent container's PointerListener grabs the selected item, sets fields in a different child container, and creates a new LazyPhotoView which works, but but does not render in time.
Is there a way to update an existing LazyPhotoView with a different image, similar to the way you showed me here ?
I want to make LazyPhotoView renderNow(), after updating its lazy loaded image.Stan
04/03/2023, 8:42 PMStan
04/03/2023, 8:42 PMNick
04/04/2023, 5:37 AMNick
04/04/2023, 3:38 PMSmallRing store a Deferred<Image> instead of an Image. This way SmallRingView could become a container and hold a Label and a LazyPhotoView. These are then updated accordingly in update. Label's text can be changed dynamically. And I modified LazyPhotoView so it's pendingImage can as well. This allows for better encapsulation of state so the ring View doesn't care about image loading.
Another thing I suggest is to use the List.selectionChanged event instead of listening to clicks on the list. This is more robust since it avoids making assumptions about the way List handles clicks. It is also simpler to maintain and clearer what's going on.Stan
04/04/2023, 4:21 PMStan
04/04/2023, 8:15 PMStan
04/04/2023, 8:17 PMStan
04/04/2023, 8:18 PMStan
04/04/2023, 8:21 PMStan
04/05/2023, 5:12 PMNick
04/06/2023, 1:53 AMsetSelection after creating the list.Stan
04/06/2023, 4:40 PMviewport? ... use css media queries ?
That is a very vague, general question. I just want to know if I can build one UI to fit both desktop and smart-phone browsers. (Hoping answer is an unqualified yes. Or just a bit qualfied.)Stan
04/06/2023, 4:42 PMStan
04/06/2023, 4:42 PMStan
04/06/2023, 5:30 PMsetSelection after creating the list."
If I list.setSelection(setOf(0)), the default selection is not highliged (colored) like it is when I click it. Is that behaviour I need to "code"?Nick
04/07/2023, 1:18 AMStan
04/07/2023, 1:32 AMNick
04/07/2023, 1:34 AMlist.selected reflect the right value?Nick
04/07/2023, 1:35 AMNick
04/07/2023, 1:41 AMNick
04/07/2023, 1:42 AMNick
04/07/2023, 1:43 AMStan
04/07/2023, 1:46 AMDynamicList().apply {
...
println("pre selection -> $selection")
setSelection(setOf(0))
println("post selection ->$selection")
}
Console indicates select is item[0]"
pre selection -> []
post selection -> [0]
There’s just no visual indication in the list.Nick
04/07/2023, 1:46 AMNick
04/07/2023, 1:47 AMStan
04/07/2023, 1:52 AMNick
04/07/2023, 1:54 AMNick
04/07/2023, 2:10 AMclass ListTestApp(display: Display, theme: Theme, themes: ThemeManager): Application {
init {
themes.selected = theme
display += DynamicList(
listOf("Hello", "World", "This is", "a Test"),
StringVisualizer(),
SingleItemSelectionModel(),
fitContent = setOf(Dimension.Height)
).apply {
bounds = Rectangle(350.0, 10.0, 300.0, 200.0)
cellAlignment = {
it.left eq 2
it.centerY eq parent.centerY
}
setSelection(setOf(0))
}
display.layout = constrain(display.children[0]) {
it.edges eq parent.edges
it.centerX eq parent.centerX
it.centerY eq parent.centerY
}
display.fill(White.paint)
}
override fun shutdown() {}
}Stan
04/07/2023, 2:11 AMapply { setSelection(setOf(0)) just above and below selectionChanged += } , but it does not trigger selectionChanged. Only clickin an item triggers selectionChanged.Nick
04/07/2023, 2:11 AMStan
04/07/2023, 2:11 AMNick
04/07/2023, 2:12 AMStan
04/07/2023, 2:14 AMNick
04/07/2023, 2:14 AMStan
04/07/2023, 2:15 AMStan
04/07/2023, 2:15 AMStan
04/07/2023, 2:15 AMNick
04/07/2023, 7:10 PMStan
04/07/2023, 7:25 PMStan
04/07/2023, 8:07 PMArgument -Xopt-in is deprecated. Please use -opt-in instead:Stan
04/07/2023, 8:07 PMStan
04/07/2023, 8:30 PMStan
04/07/2023, 8:31 PMlog4jVersion should be slf4jVersion ?Nick
04/08/2023, 2:59 AMStan
04/08/2023, 2:06 PMimport io.nacular.doodle.docs.utils.DEFAULT_FONT_FAMILIES
import io.nacular.doodle.docs.utils.DEFAULT_FONT_SIZE
import io.nacular.doodle.docs.utils.blueClick
import io.nacular.doodle.docs.utils.clickView
I'm trying to run your popup mgr demo, and hunting around your docs/assets/js dir for code. But I don't think I'm going to be successful. How can I use pkg nacular.doodle.docs.utils ?Stan
04/08/2023, 2:12 PMNick
04/08/2023, 5:46 PMNick
04/10/2023, 2:29 PMStan
04/10/2023, 2:49 PMStan
04/10/2023, 2:50 PMStan
04/10/2023, 2:51 PMNick
04/10/2023, 2:53 PMStan
04/10/2023, 3:01 PMStan
04/10/2023, 3:02 PMNick
04/10/2023, 3:05 PMStan
04/10/2023, 3:17 PMNick
04/10/2023, 3:21 PMStan
04/10/2023, 3:23 PMNick
04/10/2023, 3:23 PMStan
04/10/2023, 3:23 PMStan
04/10/2023, 3:24 PMStan
04/10/2023, 3:25 PMStan
04/10/2023, 3:26 PMNick
04/10/2023, 3:29 PMStan
04/10/2023, 3:39 PMNick
04/10/2023, 3:42 PMStan
04/10/2023, 3:43 PMStan
04/10/2023, 3:44 PMStan
04/10/2023, 5:37 PMBaseView with a pop-up drawer sytle menu.
The BaseView has an EventBus, which is passed down to the pop-up menu.
When I click on a link in the pop-up menu, the EventBus emits an event back up to the BaseView (with default currentPage = HomePage), which responds by building a new currentPage.
The event bus is working, but I don't know how to make the BaseView re-render / re-layout with the newly built (and cached) currentPage.
Here is my pop-up menu:Stan
04/10/2023, 5:37 PMStan
04/10/2023, 5:38 PMStan
04/10/2023, 5:38 PMStan
04/10/2023, 5:39 PMinit {
mainScope.launch {
eventBus.events.filter { event ->
event != null
}.collectLatest {
println("Received ${it.name} event")
when (it) {
GO_HOME -> {
currentPage = pageFactory.buildPage((HOME)) as View
relayout()
}
GO_RINGS -> {
currentPage = pageFactory.buildPage((RINGS)) as View
relayout()
}
GO_NECKLACES -> {
currentPage = pageFactory.buildPage((NECKLACES)) as View
relayout()
}
GO_SCAPULARS -> {
currentPage = pageFactory.buildPage((SCAPULARS)) as View
relayout()
}
GO_BRACELETS -> {
currentPage = pageFactory.buildPage((BRACELETS)) as View
relayout()
}
GO_EAR_RINGS -> {
currentPage = pageFactory.buildPage((EAR_RINGS)) as View
relayout()
}
GO_ABOUT -> {
currentPage = pageFactory.buildPage((ABOUT)) as View
relayout()
}
LOGOUT -> {
println("Received LOGOUT event. TODO: logout")
}
}
}
}
children += listOf(currentPage, menu)
layout = constrain(currentPage, menu)
{ currentPageBounds, menuBounds ->
currentPageBounds.edges eq parent.edges
<http://menuBounds.top|menuBounds.top> eq 10
menuBounds.right eq parent.right - 10
menuBounds.left eq parent.right - 100
menuBounds.height eq 100
}
}Stan
04/10/2023, 5:40 PMBaseView re-render with the new currentPage.
Am I doing something wrong with mainScope.launch { ... } ?Nick
04/10/2023, 7:06 PMinit {
val menu = view { }
val new = view { }
// on event
updateView(new)
// Only constrain menu since you need to add/remove constraint
// for the other view dynamically
layout = constrain(menu) { menuBounds ->
<http://menuBounds.top|menuBounds.top> eq 10
menuBounds.right eq parent.right - 10
menuBounds.left eq parent.right - 100
menuBounds.height eq 100
}
}
private fun updateView(new: View) {
val oldView = children.firstOrNull()
when (oldView) {
null -> children += new
else -> children[0] = new
}
// Only do this if you want to continue using constraint layout
(layout as ConstraintLayout).let { layout ->
oldView?.let {
layout.unconstrain(it, fill)
}
layout.constrain(new, fill)
}
}Stan
04/10/2023, 7:13 PMwhen logic be inside the BaseView's init{ ... } or outside it?Stan
04/10/2023, 7:18 PMStan
04/10/2023, 7:18 PMchildren += listOf(currentPage, menu)Stan
04/10/2023, 7:19 PMStan
04/10/2023, 7:20 PMNick
04/10/2023, 7:21 PMunconstrain as I do above (which takes the same arguments as the constrain (with the gatcha that you MUST use a lambda that is cached since inline lambdas can be a new instance each time).
Alternatively, you could just use simpleLayout, which you won't need to update at all as you swap things out, as long as the number of children remain the same:
layout = simpleLayout { container ->
val pageBonds = container.children[0]
val menuBounds = container.children[1]
pageBonds.bounds = Rectangle(size = container.size)
menuBounds.bounds = Rectangle(
x = container.width - 100,
y = 10.0,
width = 90.0,
height = 100.0
)
}Stan
04/10/2023, 7:49 PM// Only constrain menu since you need to add/remove constraint for the other view dynamically.
children += listOf(menu)
layout = constrain(menu) { menuBounds ->
<http://menuBounds.top|menuBounds.top> eq 10
menuBounds.right eq parent.right - 10
menuBounds.left eq parent.right - 100
menuBounds.height eq 100
}
// When I try this, neither the home page nor menu are displayed, even after
// calling updateView(currentPage) on the line above children += listOf(menu)
But that's OK... I don't know how many children BaseView will eventually need. So it works fine for now.
How to increase the size of Hyperlink text?Nick
04/10/2023, 7:51 PMFont w/ the right font-size. You'll need to load a font similar to how you load an image: https://nacular.github.io/doodle/docs/rendering/text#fonts.Stan
04/10/2023, 7:52 PMStan
04/10/2023, 7:57 PMfont is not a HyperLink param ???Nick
04/10/2023, 7:58 PMfont property.Nick
04/10/2023, 8:00 PMStan
04/10/2023, 8:32 PMStan
04/10/2023, 8:32 PMStan
04/10/2023, 10:37 PMStan
04/11/2023, 3:43 PMlink-font.webm video I posted above -- yesterday.)
Here, a Menu encapsulates a MenuPopup ... Just in case you're feeling altrusistic enough to take a look 😉
As you might guess, I don't grasp the meaning of Strength.Strong yet.
CheersStan
04/14/2023, 12:38 AMStan
04/14/2023, 12:39 AMStan
04/14/2023, 12:40 AMStan
04/14/2023, 12:43 AMNick
04/14/2023, 12:45 AMStan
04/14/2023, 12:47 AMcolumnSpan, but it didn't look right.Stan
04/14/2023, 12:47 AMadd(leftPanel, row = 0, column = 0, rowSpan = 10, columnSpan = 1)
add(centerPanel, row = 0, column = 1, rowSpan = 10, columnSpan = 2)
add(rightPanel, row = 0, column = 2, rowSpan = 10, columnSpan = 1)
add(footerPanel, row = 10, rowSpan = 1, columnSpan = 4)
So I'll go back to simple containers.Stan
04/14/2023, 12:50 AMStan
04/14/2023, 4:56 PMio.nacular.doodle.image.Image, like you do in your Photos tutorial? I am looking at your code, but haven't figured it out.Stan
04/14/2023, 6:27 PMNick
04/14/2023, 7:30 PMStan
04/14/2023, 7:37 PMoverride fun render(canvas: Canvas) {
image?.let { canvas.image(image = it, destination = Rectangle(0, 0, 30, 30)) }
}
Yeah... left this snippet hanging in the chat send window.Nick
04/14/2023, 7:39 PMStan
04/14/2023, 7:39 PMNick
04/14/2023, 7:40 PMStan
04/14/2023, 7:41 PMStan
04/14/2023, 7:42 PMStan
04/14/2023, 7:43 PMNick
04/14/2023, 7:47 PMStan
04/14/2023, 7:48 PMNick
04/14/2023, 7:49 PMNick
04/14/2023, 7:50 PMStan
04/14/2023, 7:52 PMGridPanel with.Nick
04/14/2023, 7:53 PMStan
04/14/2023, 7:54 PMMutableSharedFlow event buses (menu, dynamic list select events). Do you think?Stan
04/14/2023, 8:14 PMNick
04/15/2023, 1:25 AMStan
04/15/2023, 3:25 PMNick
04/15/2023, 4:39 PMStan
04/15/2023, 4:48 PMStan
04/15/2023, 4:49 PMNick
04/15/2023, 4:50 PMNick
04/15/2023, 4:51 PMStan
04/15/2023, 4:52 PMkotlin.js.compiler=ir Unresolved reference: nacularNick
04/15/2023, 4:54 PMStan
04/15/2023, 4:57 PMgradle.properties first:
doodleVersion=0.9.1-DEBUG -> doodleVersion=0.9.1Stan
04/17/2023, 4:41 PMPointerModule
that make list scrolling work on mobile browser too?Nick
04/17/2023, 7:05 PMStan
04/17/2023, 7:27 PMNick
04/17/2023, 7:31 PMStan
04/17/2023, 7:32 PMStan
04/17/2023, 7:33 PMNick
04/17/2023, 7:46 PMStan
04/17/2023, 7:49 PMselectionChanged that emits an event via an event bus, e.g.,
MutableSharedFlow<BaseProductSelectEvent>
Links coming...
Example 1
Example 2
[EDIT] Pushed a big refactoring into the main branch while you were away, in case the links do not make sense now [/EDIT]Nick
04/17/2023, 7:55 PMNick
04/17/2023, 7:56 PMResizer(this).apply { movable = false }Stan
04/17/2023, 7:57 PMNick
04/17/2023, 7:59 PMStan
04/17/2023, 8:00 PMNick
04/18/2023, 1:06 AMStan
04/18/2023, 1:39 AMStan
04/18/2023, 1:39 AMNick
04/18/2023, 1:40 AMStan
04/18/2023, 1:40 AMStan
04/18/2023, 1:49 AMNick
04/18/2023, 2:00 AMStan
04/18/2023, 2:04 AMStan
04/18/2023, 2:05 AMStan
04/18/2023, 2:05 AMNick
04/18/2023, 2:38 AMStan
04/19/2023, 9:15 PMContainer() instead of using a more complex hierrachy, i.e,
AbstractProductContainer: IProductContainer: Container() { }
I have tried to use a CompleteProductContainer subclassing AbstractProductContainer.
And CompleteProductContainer has members such as
val product: IProduct (ring, necklace, etc.)
val accessory: IProductAccessory (ring-stone, necklace-pendant, etc)
This is what I nest in the center panel... a complete ring (with stone), etc.
But when I toggle the menu, the center panel images don't consistently update.
When I simply inherit from Container(), such as here and here, menu toggling correctly updates the images in the center panel (the most recent selections).
The center panel images do not correctly switch when I toggle the menu when I use this hirearchy
CompleteProductContainer sublcasses AbstractCompleteProductContainer: Container().
Could be, probably is a bug in my "Complete" container hirearchy, but haven't been able to find it yet, and I'm beginning to suspect I am running into trouble using multiple instances of the superclass, reusing the same instance variable names. I dunno... so I'm asking this fuzzy question just in case I am hitting a limitation in javascript (which I am very ignorant of).Nick
04/20/2023, 12:34 AMMainScope instances?Stan
04/21/2023, 4:32 PMStan
04/21/2023, 4:58 PMAffineTransform and Camera. I see examples of creating perspective of content drawn inside the view.
But what about using AffineTransform and Camera for image.svg content loaded via LazyPhoto? For example, a view declares:
private val diamondImage = LazyPhoto(mainScope.async { images.load("blue-diamond.svg")!! })Stan
04/21/2023, 4:58 PMStan
04/21/2023, 4:59 PMNick
04/21/2023, 8:17 PMtransform and camera. That will make the View look like a flat surface that has some perspective depending on the transform used. You can further fake the 3d effect by having the view render slightly different images based on the camera/transform. This is how the cards in the docs work. They monitor cameraChanged and transformChanged and calculate which face is visible. They then re-render if the visible face changes.Stan
04/21/2023, 8:21 PMNick
04/21/2023, 8:25 PMLazyPhoto a transform and camera directly. Then it will look like a flat price of paper that is floating in a 3d space.Stan
04/21/2023, 8:31 PMStan
04/21/2023, 9:00 PMrect, but my drawing is a diamond:Stan
04/21/2023, 9:01 PMStan
04/21/2023, 9:02 PMStan
04/21/2023, 9:05 PMStan
04/21/2023, 9:13 PMStan
04/21/2023, 9:20 PMNick
04/21/2023, 10:09 PMStan
04/25/2023, 8:07 PMStan
04/25/2023, 8:07 PMpublic operator fun <T> invoke(
first : Field<*>,
second : Field<*>,
vararg rest : Field<*>,
onInvalid: ( ) -> Unit = {},
onReady : (List<*>) -> T): FieldVisualizer<T> = field {
Form {
this(first, second, *rest, onInvalid = { field.state = Invalid(); onInvalid() }) { fields ->
state = Valid(onReady(fields))
}
}.apply { configure(this) }
}
[EDIT Remove some noise.]
Can you tell me what I am doing wrong? I get class cast errors.Nick
04/26/2023, 12:42 AMval form = Form { this(
+field0,
+field1,
10 to field2,
+field3,
…
+fieldN
onInvalid = { … },
) { (a,b,c,…,n) -> // destructure given list
} }.apply {
…
}
You can then use list destructor to get the values out in a nice way: Parentheses around a list that gives the 0 - N value in the list. These will be untyped, so you’ll need to cast; and you’d need to implement a 7th order destructure extension for List to make this work.Stan
04/26/2023, 4:32 PMvararg param.
It works now. Thanks again for your help.Nick
04/26/2023, 4:50 PMStan
04/26/2023, 4:56 PMNick
04/26/2023, 4:59 PMString, but has 2 text fields that do validation to make sure they are the same.Stan
04/26/2023, 5:02 PMNick
04/26/2023, 5:02 PMStan
04/26/2023, 5:07 PMStan
04/27/2023, 4:39 PMfun <T> formFields() .
I have a data class:
data class PasswordConfirmation(val password: String, val confirmPassword: String) {
fun isMatch(): Boolean {
return password.isNotEmpty() && confirmPassword == password
}
}
I map the data class fields to form fields here.
I create the SetPasswordForm instance in my parent RegistrationForm here.
How do I nest these form fields in my parent form, and replace the two password TextFields here ?Stan
04/27/2023, 6:33 PMNick
04/27/2023, 9:45 PMval subForm = form { this(
initial.map { it.name } to labeled("Name") { textField() },
initial.map { it.age } to labeled("Age" ) { textField(encoder = ToStringIntEncoder) },
onInvalid = {}
) { name, age ->
Person(name, age) // construct person when valid
} }
Then in a separate file, do this.
Form { this(
+ labeled("Text" ) { textField() },
+ labeled("Number") { textField(encoder = ToStringIntEncoder) },
Person("Jack", 55) to subForm,
// ...
onInvalid = {}
) { text: String, number: Int, person: Person ->
// called each time all fields are updated with valid data
} }
Does that not work?Stan
04/27/2023, 10:48 PMval subForm = form { this( ... in a file:Stan
04/27/2023, 10:52 PMStan
04/27/2023, 11:05 PMNick
04/27/2023, 11:21 PMNick
04/27/2023, 11:23 PMPerson data class instance.
My point is you should be able to define the sub form in a separate file as long as you have the right type info in the declaration.Nick
04/27/2023, 11:26 PMdata class Person(val name: String, val age: Int)
Try that and make sure you can define this simple example as it’s own var in your separate class. Then add it to the main form. If so, you can customize the sub form to your data. You don’t need to use a data class either; any object can come from the sub form.Nick
04/27/2023, 11:27 PMNick
04/27/2023, 11:30 PMvar sub = form<Person> {
…
}Stan
04/28/2023, 12:11 AMvar subForm = form<PasswordConfirmation> {
form {
this(
initial.map { it.password } to labeled("Password") { textField() },
initial.map { it.confirmPassword } to labeled("ConfirmPassword") { textField() },
onInvalid = {
// ??
}
) { password, confirmPassword ->
PasswordConfirmation(password, confirmPassword)
}
}
}Nick
04/28/2023, 5:23 AMfield easily. Just use that function and have it return a View. You get the initial value of the field via the initial variable that is available in the field builder block. You set the field’s state using the state variable.
initial.ifValid { … } // set initial value to your text-field after validating
state = Valid(“validated password”) // set the field to valid if both text fields match
state = Invalid() // if password invalid or they don’t match
Take a look at how this works in the textField impl.Stan
04/28/2023, 12:56 PMStan
04/28/2023, 1:06 PMCPF field, and probably need to make it a custom field.
A CPF is equivelant to a US Social Security Number, in format
000.000.000-00
Here is a data class to represent a CPF.
I stubbed out this "sub" CPF Form . It needs to show four input fields in a horizontal layout:
XXX (dot) XXX (dot) XXX (dash) XX.
This is not going to work.
Do you recommend I create a subform that uses a custom field implementation (you are pointing me to), or is there a simpler way, based on the CPF Form ?Nick
04/28/2023, 1:06 PMNick
04/28/2023, 1:11 PMStan
04/28/2023, 1:16 PMStan
04/28/2023, 4:08 PMval customCpfField = cpfField(StyledText("CPF", config.formTextFieldFont, Black.paint))
private val mainForm = Form {
this(
+labeled(
name = "Nome Completo",
help = "9+ alpha-numeric characters",
showRequired = Always("*")
) {
textField(
pattern = Regex(pattern = ".{9,}"),
config = textFieldConfig("Informar seu nome completo")
)
},
customCpfField, /* ??????????????????? */
+labeled(
name = "Dummy CPF",
help = "14 characters",
showRequired = Always("*")
) {
textField(
pattern = Regex(pattern = ".{14,}"),
config = textFieldConfig("Informar seu CPF, no formato DDD.DDD.DDD-DD")
)
},Stan
04/28/2023, 4:15 PMNick
04/28/2023, 8:05 PMval form = Form { this(
true to switchField("Wasn't that "..boldFont("easy").."?"),
onInvalid = {}
) { bool: Boolean ->
println("Form valid: $bool")
} }Stan
04/29/2023, 10:02 PMStan
04/29/2023, 10:07 PMfun <T> LabeledConfig.textFieldConfig(...) for the 2nd, custom field I placed here.
The validation of the sub-fields and the final cpf string value works, but I can't figure out how to show the red help text when I tab through CPF sub-fields, leaving invalid values in them.
Can I do what I am trying to do? Or should I say, "can it be done"?Nick
04/30/2023, 1:21 AMLabelConfig into your CPF field and update the help text on it whenever the CPF changes to these states (in validateCpf it seems). This is essentially what the doc example does. It passes a custom TextFieldConfig into the textField that updates the `LabelConfig`’s help text when the textField changes state: `onInvalid`/`onValid`.
fun <T> LabeledConfig.textFieldConfig(placeHolder: String = "", errorText: StyledText? = null): TextFieldConfig<T>.() -> Unit = {
val initialHelperText = help.styledText
help.font = smallFont
textField.placeHolder = placeHolder
onValid = { help.styledText = initialHelperText }
onInvalid = {
if (!textField.hasFocus) {
help.styledText = errorText ?: it.message?.let { Red(it) } ?: help.styledText
}
}
+labeled("Name", help = "3+ letters") {
textField(Regex(".{3,}"), config = textFieldConfig("Enter your name")) // pass adapter that sets help text when field state changes
}
You won’t need to refer to TextFieldConfig in your case of course. You should just take a LabeledConfig into your custom field directly.
You will have an instance of LabelConfig (with help text) inside the labeled builder that you’re using.Stan
04/30/2023, 4:20 PM> RE: You won’t need to refer to TextFieldConfig in your case of course. You should just take a LabeledConfig into your custom field directly.Lost... Can you help? (Again.) I define a
fun <T> LabeledConfig.cpfFieldConfig here.
I pass it in here.
I pass it all the way down into fun valiidateCpf(), but how do I change the help text here? Or anywhere?Nick
04/30/2023, 4:37 PMLabeledConfig, NOT a TextFieldConfig. You should be able to pass this (the this reference should already point to a LabeledConfig) directly into the CPF here (and all the way down). And change CPF (and other layers) to take LabeledConfig instead. Then you can simply do
labelConfig.helper.styledText = …
Inside validateCpf.Stan
04/30/2023, 7:01 PMStan
04/30/2023, 7:02 PMStan
04/30/2023, 10:34 PMNick
04/30/2023, 11:02 PMStan
05/05/2023, 6:58 PMStan
05/05/2023, 7:00 PMNick
05/05/2023, 8:30 PMStan
05/06/2023, 12:11 AMStan
05/14/2023, 8:55 PMStan
05/14/2023, 8:56 PMNick
05/14/2023, 9:16 PMNick
05/14/2023, 9:17 PMStan
05/14/2023, 9:26 PMStan
05/14/2023, 9:27 PMgetElementById('xyz-field') to do anything: input text, push buttons, etc...Stan
05/14/2023, 9:28 PMNick
05/14/2023, 9:35 PMstate property up-to-date. end-end testing would require some more complex mocking of doodle itself. this is something i’d like to create a lib for. essentially, you’d need a harness that allows you to send mouse, key events etc to an app, without having to know anything about its internals.Stan
05/14/2023, 9:35 PMcommonMain.Nick
05/14/2023, 9:36 PMStan
05/14/2023, 9:37 PMStan
05/14/2023, 9:38 PMStan
05/14/2023, 9:40 PMStan
05/14/2023, 9:43 PMStan
05/15/2023, 12:42 AMmockk in commonTest to make views send requests to the server? If true, that would be good-enough to allow me to create functional (end2end) tests.Nick
05/15/2023, 1:07 AMmockk tests in jvmTest. but that’s not an issue since you can test stuff that’s in commonMain from there. then you can mock your backend tier to see how your views interact with it.Stan
05/15/2023, 1:45 AMjvmTest, and they can submit forms in commonMain that send requests back to jvmMain, which in turn send requests to a real (fake data) back-end too (not mock-only) ?
jvmTest -> commonMain --> ktor-http --> jvmMain --> http -> real-rest-service --> (REST request)
|
\/
jvmTest <--http <-- commonMain <-- ktor-http <-- jvmMain <-- http (REST response)
I can create tests in jvmTest that send REST requests to a real back-end service like the flow above?Stan
05/15/2023, 2:07 AMjvmMain is dedicated to the front end only. Only jvmMain will make rest calls to back-end services like UserService, ProductService, PaymentService, etc... I am going to try the _BACK-END-FOR-FRONT_-_END_ BFF pattern -- the UI will get all of its JSON from `jvmMain`; it will not make lots of little requests to the quasi-micro services (that's the job of jvmMain).
Does that make sense to you?
Some BFF pros and cons are described here: https://akfpartners.com/growth-blog/backend-for-frontendNick
05/15/2023, 2:08 AMStan
05/15/2023, 9:49 PMval fontLoader: FontLoader = _FontModule_.???
Likewise, for other modules, conventionally installed in an App? I want real modules, without having to bootstrap a test App.Nick
05/16/2023, 2:34 AMStan
05/16/2023, 12:41 PMStan
05/18/2023, 5:00 PMemailControl.emailField(labelConfig = this).
But I need to pass an instance I create in the test case, not this.Nick
05/18/2023, 8:00 PMFieldInfo and trigger its stateChanged to see if your emailField will update the returned TextField . you can also modify the TextField directly and make sure the emailField updates the fieldInfo.state correctly.
@Test fun `form field`() {
var state = Valid("Foo")
val stateListener = slot<ChangeObserver<Field<String>>>()
val fieldInfo = mockk<FieldInfo<String>>().apply {
every { this@apply.state } answers { state }
every { this@apply.stateChanged } returns mockk<ChangeObservers<Field<String>>>().apply {
every { this@apply += capture(stateListener) } answers {}
}
}
val config = mockk<LabeledConfig>()
val emailField = emailField(config)
val view = emailField(fieldInfo) as TextField
// validate view
verify { fieldInfo.state = any() } // validate state is set properly
// update state and notify your field
state = Valid("Foo")
stateListener.captured(mockk<Field<String>>().apply {
every { state } returns state
})
// validate that it does the right stuff etc.
expect("foo") { view.text }
}Stan
05/18/2023, 8:57 PM// validate view bits, but I think I am going to be able to figure it out in EmailControlTest.ktStan
05/18/2023, 9:53 PMlateinit property captured has not been initialized
Can you help de-confuse me about this mockk feature?Nick
05/19/2023, 1:27 AMstateChanged in your email field. this should be considered a bug since it means you won't be able to handle changes in state of the underlying data. you could conditionally use the captured value as follows though:
if (stateListener.isCaptured) {
// ...
}
Mockk lets you capture inputs in your every blocks. this is what i showed with this part of the code:
every { this@apply.stateChanged } returns mockk<ChangeObservers<Field<String>>>().apply {
every { this@apply += capture(stateListener) } answers {}
}
but that captured value will only be initialized if the FieldInfo.stateChanged += is called, which your impl currently doesn't do.Nick
05/19/2023, 1:43 AMStan
05/19/2023, 6:50 PMstateChanged in your email field"
You mean in my custom control?
this += TextField()._apply_ *{*
initial._ifValid_ *{*
text = *it*
}
// Notifies of changes to the Field's state.
stateChanged += *{*
if (*it*.state is Form.Valid<String>) {
labelConfig.help.styledText = defaultHelpMessage()
} else {
_println_("no op")
}
}
...
textChanged += *{* _, _, new *-> ... }*
focusChanged += *{* _, _, hasFocus *-> ... }*
...
}
I added that stateChanged += block, but it is never invoked, and I don't see any FieldInfo.stateChanged examples in your doodle lib, or tests, or tutorial code.
(2) "but that captured value will only be initialized if the FieldInfo.stateChanged += is called, which your impl currently doesn't do."
You mean my test impl? My custom control impl?
Sorry to bug you.Nick
05/19/2023, 7:19 PMstateChanged in the custom control are what i meant. your test should "work" (haven't tried to compile it) if you add your control to a Form (recommended, sorry i shared a more low-level way to do this earlier using a mocked FieldInfo).Nick
05/19/2023, 7:20 PMForm directly in the test 😅Stan
05/19/2023, 11:12 PMNick
05/20/2023, 1:11 AMNick
05/20/2023, 1:23 AMForm like the tests i shared for Doodle. you would then use the FieldInfo mock in other tests to see that the Textfield changes result in state being set to Valid and Invalid as expected. that’s because the FieldInfo approach is the only one that gets you access to the returned Container (and underlying Textfield) from your field.Stan
05/20/2023, 4:32 PMstateChanged += *{*
_println_("EmailControl.stateChanged was called")
if (*it*.state is Form.Valid<String>) {
labelConfig.help.styledText = defaultHelpMessage()
} else {
_println_("no op")
}
}
My test's stateListener is captured:
// You'd have to call it yourself in the test (if it's captured).
// This event lets your field know when the underlying state changes.
if (stateListener.isCaptured) {
_println_("YES: stateListener.isCaptured")
fieldInfo.stateChanged = ? // How do I call fieldInfo.stateChanged? What value to I set it to?
} else {
_println_("NO: stateListener.isCaptured")
}
How do I call fieldInfo.stateChanged? What value to I set it to?
I have a test case that set's the initial email addr to an invalid addr, but the control thinks it is valid, and I cannot call stateChanged in the control's TextField -- don't know how.Nick
05/20/2023, 4:38 PMstateListener.captured will return the captured value which should be of type: ChangeObserver<Form.Field<String>>. so you can invoke it like this: stateListener.captured(field). i'd recommend creating a mockk<Field<String>> to pass in and just make sure it's state is the same as the one in FieldInfo.
var state = Valid("Foo")
val field = mockk<Field<String>>().apply {
every { this@apply.state } answers { state }
}
val stateListener = slot<ChangeObserver<Field<String>>>()
val fieldInfo = mockk<FieldInfo<String>>().apply {
every { this@apply.field } returns field
every { this@apply.state } answers { field.state }
every { this@apply.initial } returns Valid("Start")
every { this@apply.stateChanged } returns mockk<ChangeObservers<Field<String>>>().apply {
every { this@apply += capture(stateListener) } answers {}
}
}
val config = mockk<LabeledConfig>()
val emailField = emailField(config)
val textField = (emailField(fieldInfo) as Container).first() as TextField
// validate textField
expect("Start") { textField.text }
// update state and notify your field
state = Valid("Foo")
if (stateListener.isCaptured) {
stateListener.captured(field) // <========== call listener w/ underlying field
verify { fieldInfo.state = any() } // validate state is set properly
}
// validate that it does the right stuff etc.
expect("Foo") { textField.text }Nick
05/20/2023, 4:40 PMNick
05/20/2023, 4:43 PM@Test fun `ensure defaults to blank`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
+ emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 1) { onValid ("") }
verify(exactly = 0) { onInvalid( ) }
}
@Test fun `ensure accepts valid initial`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
"<mailto:foo@bar.com|foo@bar.com>" to emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 1) { onValid ("<mailto:foo@bar.com|foo@bar.com>") }
verify(exactly = 0) { onInvalid( ) }
}
@Test fun `ensure rejects invalid initial`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
"foo" to emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 0) { onValid (any()) }
verify(exactly = 1) { onInvalid( ) }
}Nick
05/20/2023, 4:45 PMFieldInfo approach in your other tests when doing the other tests: checking that changes to the TextField result in f`ieldInfo.state` being set to Valid("YOUR_VALID_INPUT") or Invalid().Stan
05/20/2023, 5:11 PM_verify_(exactly = 1) *{* onValid(blank) *}* fails:
Verification failed: call 1 of 1: Function1(#28).invoke(eq())) was not called
This is what creates the field.Nick
05/20/2023, 5:15 PMStan
05/20/2023, 5:16 PMNick
05/20/2023, 5:17 PMinitial is not a valid email.Stan
05/20/2023, 5:17 PMNick
05/20/2023, 5:19 PMNick
05/20/2023, 5:19 PMNick
05/20/2023, 5:20 PMStan
05/20/2023, 5:20 PMStan
05/20/2023, 5:20 PMStan
05/20/2023, 5:20 PMStan
05/20/2023, 5:22 PMStan
05/20/2023, 5:24 PMNick
05/20/2023, 5:24 PMStan
05/20/2023, 5:28 PMNick
05/20/2023, 5:35 PMStan
05/20/2023, 5:40 PM_verify_(exactly = 1) *{* onValid(blank) *}* fails.
Should that line in the test be _verify_(exactly = 0) *{* onValid(blank) *}*?
In that case, zero onValid and onInvald calls can be verified. And in the test case, I have no way of knowing if the email-field is valid or not (back in circle to mockk'd fieldInfo?)Nick
05/20/2023, 5:49 PMverify(exactly=0){ onValid(any()) }. no calls to onValid.Nick
05/20/2023, 5:50 PMStan
05/20/2023, 5:51 PMStan
05/20/2023, 5:56 PMStan
05/20/2023, 6:07 PMNick
05/20/2023, 6:23 PMNick
05/20/2023, 6:24 PMStan
05/20/2023, 6:32 PMNick
05/20/2023, 6:42 PMNick
05/20/2023, 6:43 PMNick
05/20/2023, 6:47 PM@Test @Ignore fun `ensure defaults to invalid`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
+ emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 0) { onValid (any()) }
verify(exactly = 1) { onInvalid( ) }
}
@Test @Ignore fun `ensure accepts valid initial`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
"<mailto:foo@bar.com|foo@bar.com>" to emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 1) { onValid ("<mailto:foo@bar.com|foo@bar.com>") }
verify(exactly = 0) { onInvalid( ) }
}
@Test @Ignore fun `ensure rejects blank initial`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
"" to emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 0) { onValid (any()) }
verify(exactly = 1) { onInvalid( ) }
}
@Test @Ignore fun `ensure rejects invalid initial`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
"foo" to emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 0) { onValid (any()) }
verify(exactly = 1) { onInvalid( ) }
}Nick
05/20/2023, 6:56 PM@Test fun `text field set to blank on invalid initial`() {
validate(Valid("foo"), "")
}
@Test fun `text field set to initial when it is valid`() {
validate(Valid("<mailto:foo@blah.com|foo@blah.com>"), "")
}
private fun validate(initial: FieldState<String>, expected: String) {
val field = createField(initial)
val fieldInfo = mockk<FieldInfo<String>>(relaxed = true).apply {
every { this@apply.field } returns field
every { this@apply.state } answers { field.state }
every { this@apply.initial} returns field.state
}
val config = mockk<LabeledConfig>()
val emailField = emailField(config)
val textField = (emailField(fieldInfo) as Container).first() as TextField
// validate textField
expect(expected) { textField.text }
}
private fun <T> createField(state: FieldState<T>): Form.Field<T> = mockk<Form.Field<T>>().apply { every { this@apply.state } returns state }Nick
05/20/2023, 7:00 PM@Test fun `setting valid text works`() {
val fieldInfo = mockk<FieldInfo<String>>(relaxed = true)
val config = mockk<LabeledConfig>()
val emailField = emailField(config)
val textField = (emailField(fieldInfo) as Container).first() as TextField
// mimic user input
textField.text = "<mailto:foo@blah.com|foo@blah.com>"
// the field should be valid now
verify(exactly = 1) { fieldInfo.state = Valid("<mailto:foo@blah.com|foo@blah.com>") }
}Nick
05/20/2023, 7:01 PMNick
05/20/2023, 7:01 PMrelaxed=true for mockk is needed to allow mockks to return reasonable defaults if you don't provide an explicit mock value. super helpful to make your tests more focused and easier to read.Nick
05/20/2023, 7:06 PMpurpose field for TextField that lets you say what it is meant for. it will allow values like Email, Url, Integer, etc.. it will be helpful to specify that in your case when you upgrade; since it will enable custom keyboards on mobile.Stan
05/20/2023, 11:38 PMNick
05/20/2023, 11:57 PMStan
05/21/2023, 6:47 PMNick
05/21/2023, 7:01 PMFieldVisualizer and exposes the textfield. over time i’d also suggest not caching the `FieldInfo`; but not a big deal for now i’d say.
it also occurs to me that you might be able to just use the built-in textField with a regex/your email validator. is there a reason you didn’t in this case? the UserPasswordControl makes sense since it has custom validation between two textfields, but it feels like you could get away with the built-in for the email field.
thanks for trying out the snapshot as well! let me know if you see any issues. and play around with TextField.purpose as well.Stan
05/21/2023, 8:01 PMNick
05/21/2023, 8:13 PMStan
05/21/2023, 8:43 PMFieldVisualizer. I want to get this right before I go replicating crap 50x.Nick
05/21/2023, 8:47 PMStan
05/22/2023, 4:00 PMFieldVisualizer<String> in this commit.
Now I'm lost again. What does this buy me? How do I use invoke() to expose the TextField, instead of caching it as I am?
Sorry!Nick
05/23/2023, 1:29 AMEmailControl type and those properties could be made internal (and access them like you do now). but this isn’t a useful unless you plan on making these controls a part of a lib that you use in other places. then non-test code in other modules wouldn’t have access.
i’m mostly in library mindset. so you can ignore some of this stuff that is less important for an app.Stan
05/23/2023, 1:25 PMStan
05/26/2023, 5:56 PMNick
05/26/2023, 7:35 PMStan
05/26/2023, 8:21 PMStan
05/26/2023, 8:24 PMNick
05/27/2023, 12:23 AMString out when it should.
fun passwordField(): FieldVisualizer<String> = labeled("Password", "") {
var password = ""
var secondary = ""
val checkMatch = {
if (secondary.isNotBlank() && password != secondary) {
help.styledText = Red.invoke("Password does not match")
}
}
field {
Form { this(
+ textField(Regex(".+")) {
textField.placeHolder = "Enter password"
onValid = { password = it; if (!textField.hasFocus) checkMatch() }
onInvalid = { if (!textField.hasFocus) help.styledText = Color.Red("Invalid Password") }
},
+ textField(Regex(".+")) {
textField.placeHolder = "Re-enter password"
onValid = { secondary = it; if (!textField.hasFocus) checkMatch() }
},
onInvalid = {}
) { password, verified ->
state = when (password) {
verified -> Valid(password).also { this@labeled.help.text = "" }
else -> Invalid()
}
} }.apply {
layout = verticalLayout(this, spacing = 12.0, itemHeight = 32.0)
}
}
}Stan
05/28/2023, 5:32 PMStan
05/29/2023, 11:51 PMNick
05/29/2023, 11:54 PMfocusable = false in the apply block.Stan
05/29/2023, 11:56 PM.apply { focusable = false } Works!
And to save vertical space (on smart-phones), can I align the 2 fields horizontally, instead of vertically? With a "Confirm" label between them?
I got that working in a container, but not a form.Stan
05/29/2023, 11:58 PMNick
05/30/2023, 12:13 AMcontainer provided to its layout).
you could wrap the verification textField in a labaled with the “Confirm” text and a custom layout (specified via the NamedConfig you pass into it). that layout would put the label text to the top or left of the textfield based on conditions you specify (say width of the parent).Stan
05/30/2023, 4:10 PMNamedConfig in my +labeled ?
Is there a way to set my inner form's size (the width) based on the outer/"parent" form's size? I ask here.
And due to my confusion, my layout does not work; it just shows the primary password, and not the confirmation password.
I think I'm close, but can you hold your nose and hold my hand a little? Again. 😳Nick
05/30/2023, 4:50 PMlabeled is within the context of a NamedConfig (or LabeledConfig in this specific version of the call) so you can access it there as though it is the this reference.Stan
05/31/2023, 2:27 PMtextField.placeHolder = "Confirme sua senha"
is plenty of info for letting the user know what the 2nd pwd field is for.
Now I'm trying to use textField.mask = '*' (Optional character to mask text shown in the field. This only affects displayText). It result in no change to the clear-text I see while I type chars in the field. Should I be setting:
textField.text = textField.displayText ?
I also tried out textField.purpose = TextField.Purpose.Password,
but it resulted in a lot of behaviours in the browser I didn't not want to see, including corrupting the copy/pasted value. I/we can go into details sometime -- I'm sure you want to work out bugs, if that is indeed what I am seeing.Stan
05/31/2023, 10:23 PMNick
06/02/2023, 1:28 AMNick
06/10/2023, 2:52 AMpurpose to Password. will be fixed in the next version. what issues do you see when using purpose though?