The doc samples are a bit outdated. That visualize...
# doodle
n
The doc samples are a bit outdated. That visualizer took a
TextMetrics
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.
s
I see that. Thanks for getting back to me. It's almost beer-thirty... I'm looking at tests, src, trying to figure out how to make it work. Here's my latest hack.
TabbedPanelView.kt
I'm also wondering if I can use StyledText in the mapping:
private val mapping = mapOf( object1 to "Circle", object2 to "Second Tab", object3 to "Cool Photo", object4 to "Tab 4" )
It compiles/runs but doesn't render.
n
A few things... First, you can just use the
BasicTabbedPanelBehavior
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:
Copy code
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.
s
Very thorough, quick answer. I will try it later today. Could you point me to src, test, or demo code that will
install basicTabbedPanelBehavior as a module
? I'm new to kotlin, and interpret "installing a module" as importing a dependency via gradle .
n
For sure. Doodle uses Kodein for dependency injection. Many of its features are available through Kodein `Module`s that you “install” when invoking the
application
method. You can see how this works for themes here.
This doc page might also be helpful for thinking about themes: https://nacular.github.io/doodle/docs/display/gotchas.
s
Thanks. That helped me render a TabbedPanel with StyledText labels. I'm now trying to figure out how to see (and customize) the views displayed under each tab. First, struggling with where to correctly position and size a tab's view, set background color, etc. Things may "just work", but I need to see view borders, etc... I'm guessing the trick is to implement my own TabProducer and TabContainer params passed into basicTabbedPanelBehavior(?).
install-modules.png
n
The panel uses a visualizer to render each tab’s contents. The
View
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.
s
Copy code
Thanks 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?
Copy code
Here is a pic of the layout, after browser refresh:
Copy code
Here is a pic after I re-size browser.  The nested views are not re-sizing themselves.
The view declaring the TabbedPanel: https://github.com/ghubstan/doodle/blob/gnatty-app-a/Natty/src/commonMain/kotlin/io/dongxi/natty/tabbedpanel/TabbedPanelView.kt
Copy code
```
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"?
n
Nice! And thanks for sharing the code, it makes helping much easier. I think the issue is that you're using a
ScrollPanelVisualizer
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.
Actually, there's a bespoke builder for these kinds of TabbedPanels (with Views as their data type) that you could use directly.
s
I tried this:
Copy code
private 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 }
}
But it still does not resize the nested views.
Going back to this:
Copy code
private val tabbedPanel = TabbedPanel(
    ScrollPanelVisualizer(),
    styledTextTabVisualizer,
    homeView,
    ringsView,
    necklacesView,
    scapularsView,
    braceletsView,
    earRingsView,
    aboutView
).apply {
    size = Size(500, 300)
    Resizer(this).apply { movable = false }
}
I could not replace the
ScrollPanelVisualizer()
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?
n
You should be able to just pass it without the parentheses. Did your views show when you used the alternative invoke?
s
Yes, the views show, but do not resize (using the alt invoke or ViewVisualizer instead of ScrollPViz).
OK,
ViewVisualizer
param passed, but nested views still don't resize either.
n
K, it sounds like the issue might be in your views themselves. I’ll take a deeper look when i have some free time later or in the morning.
s
You have helped a lot on a weekend. Thanks. I stop here now as well. Here is is a link to current (and commented) TabbedPanel declarations: https://github.com/ghubstan/doodle/blob/gnatty-app-a/Natty/src/commonMain/kotlin/io/dongxi/natty/tabbedpanel/TabbedPanelView.kt#L158
n
Ok. So there a few issues. One is with your layouts and not the TabbedPanel. You have a bug where you are constraining
this
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
.
s
Wow, fixing "bug" (constrain(this)) had immediate effect. Views are resizing... I will start working on fixing the display.width -> parent bugs now. Thanks!!!
When I set a nested views initial Size(w,h) , in
apply {}
, should I be using
size = Size(parent.width / 3, parent.height - 105)
instead of
size = Size(display!!.width / 3, display!!.height - 105)
n
In your case it makes more sense to rely on the parent size and not have display passed down to the leaves at all. You might also be able to avoid setting an initial size since your layout should cover this.
s
Major improvement, due to your forebearance. It behaves well. Random hidden tab views do not re-size untill I dbl-click the nested view, but I'm not going to worry about this (now).
Do you know about Gradle's parallel build? I'm a trying it now; it reduces build time by 50%+.
My incremental (IDE) builds are <= 1ms.
Much better.
basic-layout.webm
n
haven’t tried parallel builds. but sounds like i should!
s
Add
org.gradle.parallel         = true
to
gradle.properties
.
TabbedPanel looks great on desktop & ipad safari, but won't work on iphone safari. Seems that for mobile browser, I need to use an icon representing a "drawer navigator". Just learned the word today, even though I've seen 100s of them on the web. I'm looking at https://nacular.github.io/doodle/docs/ui_components/overview#dropdown , and wondering if it can be customized to navigate. I don't see another doodle ui-component that comes close. Or should I just create an SVG icon that looks like a drawer, and drops down the nav-links to navigate to the same Views my TabbedPannel does. I know you know what I'm talking about (drawer navigation). You have a suggestion?
A one-icon version of the
TabStrip
that triggers a drop-down (menu) might be one way....
n
You could also do a button with icon that shows a popup. 0.9.1 has a new PopupManager that would make this a bit simpler. There’s only a SNAPSHOT release of it so far though.
s
It seems I cannot access
SNAPSHOT
releases. I can only see the
master
branch. The
master
branch has a
PopupMenu
. Could that work too? Is
PopupManager
distinctly different than
PopupMenu
?
n
It’s actually only published to Maven for now. The existing PopupMenh is different.
I posted about it here.
s
More baby steps before I try unreleased code... Trying to learn how to use your widgets. How do I give this button a color?
how-set-btn-color.webm
n
The behavior you set for it fully controls the way it renders. You could have it draw a rect w/ a different color when the pointer is not over the button:
Copy code
simpleTextButtonRenderer(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.
s
Hello Nick, In your
Photos
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.
n
Hey Stan. One way to do this would be to use something like this (which I might just add to the framework itself, and write a tutorial to demonstrate).
Copy code
class 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:
Copy code
val lazyImage = LazyImage(mainScope.async { images.load("foo.jpg")!! })
s
Lovely.
Or can I? Should I? Call something like 'this.relayout' at the end of
Copy code
fun loadImageInMainScope() = mainScope.launch { ... }
?
I will try your suggestion first.
n
Forgot to include this as well:
Copy code
@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.
s
I'm constantly tripping over layouts. But your solution loads/renders very well. Thanks.
n
Can you share more of what’s tripping you up? Is it just wrestling with getting things right in your app? Or do you feel layouts themselves are hard to work with?
s
"Can you share more of what’s tripping you up? Is it just wrestling with getting things right in your app? Or do you feel layouts themselves are hard to work with?" I don't think doodle's layouts themselves are hard to work with. It's down to me not having experience building UIs in any language. (I can wire them up to back-ends just fine, but front-end widget building and layout is something I am only now forcing myself to do.) I once tried to learn dojo-js because I liked its modular (oop) approach, but I did not stick with it. Kotlin and Doodle are much more approachable to me. I will get the hang of it if I keep at it. I'm more comfortable with layouts than I was yesterday ;-/ My current playground is in the view and widget packages here: https://github.com/ghubstan/doodle/tree/gnatty-drawer-menu/NattyWidgets/src/commonMain/kotlin/io/dongxi/natty. I am going through the UI-Components catalog (https://nacular.github.io/doodle/docs/ui_components/overview). I've hacked through Label, TextField, PushButton, ... up to Slider, skipping the FileSelector and others for now.
labels-to-sliders.png
I would really like to skip to your new PopupManager in v0.9.1, and see if I can build a menu navigator for iphone safari. But no access to its source code yet, right?
n
That's right; just the binary itself for now. I'm targeting a release by end of next week. 🤞
s
Hi Nick, I'm back ;-) I am looking forward to the new release! So, now I'm trying to figure out how to use your (lazy-loaded) DynamicList . The LazyPhotoView works great (here). But how do I lazy-load a DynamicList? My latest hack is here .
How would a RingVisualizer be defined?
n
So your
LazyPhotoView
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.
Copy code
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:
Copy code
class RingView(ring: Ring): View() {
    fun update(ring: Ring, index: Int, selected: Boolean) {}
}
Copy code
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)
    }
}
Also check out the PhotoStream tutorial if you haven’t already. It has a list model that fetches from unsplash and updated internal state over time. That state is made visible to observers (i.e. the list using it) via notifications to the changed listeners.
s
Thanks... Some progress. Can you tell me why the last item is not F, instead of cycling back to A?
I know I am pestering you. I have spent a lot of time with tutorials (and Photos); trying to get over big learning curves. If I can get these dynamic lists to work with observers/event listeners, I will be able to cover a big part of my use case.
n
No worries. Happy to help when I have time. I think the issue is that you’re not updating your
SmallRingView
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.
s
I need to change immutable constructor args to mutable args, and re-set them in the update method, like this? It work(!!), but not sure I'm doing it right.
Cool. It's nice to actually see something, instead of just back-end code, logs, and test results!
EDIT long, tedious question about PointerEvents -> Tomorrow, could you glance at how I capture a clicked event in one of three sibling containers? How would I use my
ClickedRingRecognizer
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.
n
It depends on what you’re trying to do. You can know which item in the List is selected by: 1. listening to List.selectionChanged 2. listening directly to the SelectionModel.selectionChanged 3. Or just reflecting the right state in your
SmallRingView
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.
s
Hi Nick, I should have left the computer and come back hours later before asking that last question. I already knew the answer but was too lost in kotlin/doodle-land to recall. Apologies... You showed me how to load a DynamicList of SmallRing. And you showed me how to render a LazyPhotoView. Now I want to update the
pendingImage
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.
The use case is: (1) select a tiny image in dynamic list (2) show large image based on selection. The picture below shows I selected tiny image C in left panel, and center panel shows correct name of selection C, but the center panel's large image still shows item A (while large image C is succussfully lazy-loaded, but not rendered in time).
update-big-view.png
n
Hey @Stan. I’ll try to take a look at this in the morning and provide some guidance. But what you’re trying should be doable without too much trouble.
Made some changes and put them in a zip. Sorry, but it was faster to go this route than cloning the repo and making a pull request. One key modifications were to have
SmallRing
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.
s
Works very well. Thank you!
yr-dynamic-lists.webm
After I tidy up the page pkg, I'll have some questions about doing this or that correctly. But don't look now!
I need the jewelry maker to send me some tidy pictures of her bling, now that she can see something less abstract.
The next thing I need to do is figure out how you place selected ring stones (right panel) on the top of the big ring image (center panel). I can probably learn from your Photos tutorial.
How would I set a default, initial selection (1st item) in a DynmicList?
n
DynamicList is a Selectable. So you should be able to call
setSelection
after creating the list.
s
I need to make UI fit in smart-phone browsers. Have you/do you create doodle based UIs for iphone & android? ... fit content to
viewport
? ... 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.)
By the way... here are two of your dynamic lists working to show ring+stone selection combinations. 👇
rings.webm
RE: "DynamicList is a Selectable. So you should be able to call
setSelection
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"?
n
Hmm. Point me to the code so i can take a look. This Should Work®.
s
I've tried to list.setSelection() within this apply {} block, and a couple of other places. It compiles, runs w/out error, but no selection-color-change.
n
Does the
list.selected
reflect the right value?
Oh, you have a listener right there. That listener doesn’t trigger? Or it does and there’s just no visual indication in the list?
The other thing is the BasicListBehavior shows selected rows as a different color when the list doesn’t have focus. Are you seeing it selected but with a different color?
On a separate note. I’ll be releasing 0.9.1 tomorrow. So i’m keen to dig in and make sure this isn’t a bug, or fix it before the release.
This release will have a new LazyPhoto as well as a Modal manager to make showing that kind of popup even easier. This would help with your mobile menu use case.
s
Reply To https://kotlinlang.slack.com/archives/C01CJM07MGV/p1680831342060359?thread_ts=1679686479.620109&amp;cid=C01CJM07MGV I add these lines at the end of
DynamicList().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.
n
About the mobile screen question. Yes, you don’t need to do anything other than take screen size into account (via Display.size) as you build your app. The Contacts demo shows how to build this with layouts that adjust to screen size. Open that app on mobile to see what i mean.
Ok. So the state is good. Is your listener notified?
s
My listener? As in selectionChanged? Do I need to excplicitly invoke selectionChanged after I setSelection(setOf(0)) ?
n
yep, that one. you don’t need to do more than register like you’ve done. just wondering if that code runs when you update the selection.
Copy code
class 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() {}
}
s
I place
apply { setSelection(setOf(0))
just above and below
selectionChanged += }
, but it does not trigger selectionChanged. Only clickin an item triggers selectionChanged.
n
This app shows this when run for me.
s
Now I'll read what you just posted above my last comment.
n
maybe your list is empty when you do this. does your model have contents at that point?
s
OK, I believe you. I"ll try it tomorrow when I'm fresh. And maybe my model is empty. I'll check that too. I assumed it was populated, but could be wrong.
n
there's actually a bug where setting the selection before populating the list can lead to strange selection when items are added. i'm going to dig into that tomorrow as well.
s
I will dig too. Thanks!
G'
night
n
I fixed that bug I mentioned above in the release by the way. Let me know if you’re still having issues with the selection.
s
Thanks, I noticed in yr release doc. I will try it in a day or so, after I build/install a debug version of 0.9.1 into my local repo, and try out your PopupManager in a new project.
Another trivial change for your next release (maybe), in `Common.kt`:
Argument -Xopt-in is deprecated. Please use -opt-in instead:
There are lot's of compiler warnings that make me itchy. 😉
I don't mean to nitpick, I just think you want to know.
In your gradle.properties:
log4jVersion
should be
slf4jVersion
?
n
no worries. i appreciate the feedback. keep it coming. i’ll be fixing a lot of these in the next release.
s
OK, I will keep it coming, but I know you're relaxed and numb after the rush to getting the release out. Enjoy! Anyway: 1. Getting Started docs crash on Firefox (Linux) 2. The first demo / code I attempted to copy / paste into a kotline file can't compile becuase of missing depenedencies, e.g.,
Copy code
import 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
?
3. Clicking the "API Docs" link on your site results in a 404.
n
Oop; forgot to update that one link in the docs. Will fix on Monday. The samples have dependencies that are not included, so compile issues expected. The goal was to make sure the code itself had no stale APIs and compiled if all dependencies are defined. This was an issue when they were just hand written in the docs.
@Stan I'm not seeing the crash on Firefox (Linux) you mentioned for "Getting Started". It loads fine for me on Ubuntu 22.04.2 LTS (VM) using FF 111.0.1 (64-bit).
s
firefox-crash.webm
I'm using same Ubuntu version on bare-metal, with a AMD/Radeon GPU. Also, same FF version. Works fine in Google Chrome.
FYI: My own little doodle prototypes work fine on FF.
n
Ok, this seems to be that the KotlinPlayground js file doesn't download. Are you able to open https://unpkg.com/kotlin-playground@1?
s
Yes.
playground.min.js.png
n
But other pages in the docs work?
s
I'll give you a list of what does work: https://nacular.github.io/doodle/docs/applications 👍 https://nacular.github.io/doodle/docs/rendering/3d 👍 https://nacular.github.io/doodle/docs/ui_components/overview 👍 None of the Catalog demo pages crashed. 👍 Pasting any other left hand side menu link in FF address bar results in crash.
n
Really strange. The error you see has to do w/ the shared KotlinPlayground react component used to render the code snippets. I'd expect it to fail on all pages w/ code samples.
s
Wish I understood more about jscript... but I'm here to help if the task is simple enough 😉
n
Can you try loading the docs in one of FF's private windows?
s
I only use private windows. I used public on a couple, not all.
I have Ghostery and Privacy Badger running.
Funny, now, https://nacular.github.io/doodle/docs/applications does not work in private or public windows. It did earlier.
Otherwise, I get same results I listed above for private and public FF windows.
n
Can you disable ad blocker etc.? Also, can you check the network traffic as you load and see if you have kotlin-playground loading like mine shows (redirects to playground.min.js.
s
That's it. Ghostery or Badger is breaking playground. If I disable them both, everything works. After re-enabling them:
n
Interesting. I might need to revisit including that dependency as a module instead of loading the js that way (which I don't love b/c of the additional network dependency). I went that route though b/c loading the module changes the inclusion order and I lose control of the font size (JS ecosystem for you). Thanks for helping to root-cause though.
s
I would have checked ghostery, then badger, but I'm multi-tasking at the moment -- computer shoulder rehab.
PopupManager lookin' good. I'll send a video later.
Hi Nick, I have a
BaseView
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:
base-view.webm
You can't see my mouse pointer, but you can see the link go red when I click it.
Here is my BaseView init{} :
init {
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
}
}
It all works, except... I just don't know how to make
BaseView
re-render with the new
currentPage
. Am I doing something wrong with
mainScope.launch { ... }
?
n
Nice progress! The issue looks to be that your BaseView has a single child (currentPage). But you change what that variable points to when events are raised. That doesn't change which child is in the BaseView though, just moves currentPage to point away while leaving behind it's initial value as the only child. So it's expected that you see no change. You can fix this by doing something like this:
Copy code
init {
            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)
            }
        }
s
I will try it. Should that switch logic... erhmmm
when
logic be inside the BaseView's init{ ... } or outside it?
FYI: BaseView has two children, the Home/Rings/Necklaces/etc Page, and the Menu.
Copy code
children += listOf(currentPage, menu)
But it's working!
Ah, I see // Only constrain menu since you need to add/remove constraint... I will check in what works, and tidy up. Thanks!
n
It's fine to keep the logic in the init as you have it. You just need to make sure to swap out the child to the new view. Also, constraints are a hair trickier to deal w/ when the views are coming and going. do you'll have to
unconstrain
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:
Copy code
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
                )
            }
s
Copy code
// 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?
n
Give it a
Font
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.
s
current-page-renders.webm
But
font
is not a
HyperLink
param ???
n
All Views have a
font
property.
Wrapping up lunch, so will check in later this evening.
s
Thanks... I just needed to define menuLinkFont in my app config and load in appScope.
link-font.webm
I'm shutting down for the night. The next thing I need to do is find a way to make the menu/popup look ok on both desktop and iphone/safari browsers. Looks ok on phone now, wierd on desktop. All I know is that I need to use relative bounds in the layout(s). I'll but you about it tomorrow(?) Thanks again!
Hey Nick. Want to thank you again for all the help you've given me. I need a little break, and don't have concise, well thought out question(s) for you today... maybe later. But, I've enough energy to say I need to get past the one-size-fits-all (desktop-browser & iphone-browser) layout problems before I move on to anything new. And first thing is get my drawer-menu-navigator layout looking good on desk/phone. (See the
link-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. Cheers
RE: "but seems like you found a solution to the GridPanel question. is that so?" Well, no. I stepped away and took another look. Thought I figured it out, but still no...
sizing.png
Struggling to make everything visible on iphone/safari, but 1st things first... making col1 narrow, col2 fat, col3 narrow... You can see in the pic.
n
You can use a GridPanel for this and have the middle item take up mor rows & cols than the other ones. That will make it bigger. The FitPanel sizing policies will make the rows/cols equal sizes so they fit the panel. But you might consider a simple Container with a constraint layout instead since the number of items is small and well known. Otherwise you’ll mess with custom sizing policies which seems unnecessary for this case.
s
I liked GridPanel and its auto-layout. And tried
columnSpan
, but it didn't look right.
Copy code
add(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.
My is the latest checked in mess. I'll come back to it tomorrow. Thanks for responding!
Appreciate all the help... Replacing grid-panel with simple containers.. How do you scale down (resize) an
io.nacular.doodle.image.Image
, like you do in your Photos tutorial? I am looking at your code, but haven't figured it out.
Ah hah... my LazyPhotoView ->
n
Did you figure this out?
s
Copy code
override 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.
n
How about moving away from grid-panel? Did you end up using constraints instead?
s
Yeah, moved to basic Container. Do you have a smart-phone on you? I can let you look for a second.
n
i do
s
Go to "Aneis".
For long dynamic lists, I need to be able to vertical scroll (?) Haven't even look at that yet.
n
You'll need a ScrollPanel, and the nativeScrollPanelBehavior() module installed. It will be usable if you're already using a Theme
s
Just curious if it was a mess. Was it? You used iphone or android?
n
You can actually simulate smaller screens in the browser (at least Chrome and FF).
There's the option to use a fixes size, or have it be "responsive". Toggle the little phone icon on the bottom right area of FF when you have the inspector open
s
Cool, thanks. I pushed my "pr" to main. Here is what I replaced the
GridPanel
with.
n
nice
s
I'm not sure if I'm inviting trouble using a few
MutableSharedFlow
event buses (menu, dynamic list select events). Do you think?
Oof... wish I knew about "Toggle the little phone icon on the bottom right area of FF" before 😉
n
I haven’t used MutableFlowState myself. So not sure if there’s a high cost. One thing to pay attention to is bundle size. The coroutine lib could pull in a lot of dependencies. So keep an eye on that.
s
Is 2.24 MiB ridiculously large? 0.5 MB are imgs (and lot more to be added), which will eventually be moved to the server.
n
You’ll want to look at the gzip size for the js file that’s created for a production build (debug builds are usually this large). The IR compiler will help a lot with size. Hopefully it will get you into the sub-500 K range.
s
I am not using the IR compiler because I don't know how to make webpack work with it.
And... say I have a util that generates an svg image (just text), but it needs to be encoded... Do I encode as base64?
n
What’s the IR issue? I’ve been able to get it working in my projects.
s
RE IR: I forgot... even more serious problems using
kotlin.js.compiler=ir
Unresolved reference: nacular
n
Hmm. I can take a look at your repo on monday to try and understand why. I have projects they use IR just fine.
s
Thanks! git clone https://github.com/ghubstan/kotlin-doodle-prototypes.git If you try to build it, change a property in
gradle.properties
first:
doodleVersion=0.9.1-DEBUG
->
doodleVersion=0.9.1
Hi Nick, is there som analog (module) to
Copy code
PointerModule
that make list scrolling work on mobile browser too?
n
Scrolling should work on mobile w/o additional changes. Is it working on non-mobile browsers and larger screens?
s
It works for non-mobile browsers, not iphone safari. Can you give me your IP? For my firewall? I understand if you'd rather not.
n
hmm. strange. can’t share IP. can you publish a version via github pages?
s
Not sure I can do that... Already purposed for something else. I let you in now: http://dongxi.mynetgear.com:8080
There are "Colares" too.
Dummy impl is here. It is instantiated in left,center, and right panels.
n
are you consuming the pointer event anywhere?
s
Not sure... maybe nowhere. As a rule, dynamic-lists implement a
selectionChanged
that emits an event via an event bus, e.g.,
Copy code
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]
There are three types of events, consumed here, here, and here.
n
Hmm. i wonder if it's b/c you're using Resizer in a number of places. it will try to move your control when it is dragged. and it might interfere with the touch scroll in this case. try removing it and see if that helps.
or set them to be unmovable:
Copy code
Resizer(this).apply { movable = false }
s
I will. Closing firewall... just a moment.
n
let me know how it goes. wrapping lunch and heading back to the office. will check in later this evening.
s
No success yet with the list scrolling on mobile...
n
even after removing the Resizers?
s
Yes.
I did not remove them, I adjusted them: Resizer(this).apply { movable = false }
n
try removing them to rule them out.
s
OK
That's an Uber-Bingo! I let you in: http://dongxi.mynetgear.com:8080/
n
AFKB now. but it worked?
s
Sorry what is afkb -- oh... away from keyboard... yes it worked!
tnx!
I close fw hole now.
n
nice!
s
This is a very fuzzy question, but I have banged my head on the problem for many hours... Is there a limit to the "amount of abstraction" and use of inheritance in Container() subclasses? If I, say, have an abstract class subclass for different "complete" product views (ring+stone, necklace+pendent, etc.), am I going to run into javascript problems? I find myself being forced to simply subclass
Container()
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).
n
There shouldn’t be any such limitation. Have you dug into the use of coroutines and the event pipeline? Maybe something is lost with the use of additional
MainScope
instances?
s
I could not figure it out, but using simple impls for specific product types (subclassing an abstract superclass) works. I just can't figure out how to use 1 impl for all specific product types, and that's going to have to be OK for now. Moving on.... Thanks.
I do have a question about 3D capabilities through use of
AffineTransform
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")!! })
Here is the svg file:
Is there anything interesting I can do with that, using AffineTransform and Camera, or not -- because the content is not acctually drawn inside the containing view. I am wanting to know if it is possible for svg image file content to be displayed in 3D space. I suspect not, but wanted to get your confirmation.
n
So you can display the view that draws the LazyPhoto in a 3d space by giving it a
transform
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.
s
Give me hope! I will investigate. So I need a custom LazyPhoto that has a transform and camera -- in the same view containing the lazy-load image (?)
n
No; you should be able to give the
LazyPhoto
a
transform
and
camera
directly. Then it will look like a flat price of paper that is floating in a 3d space.
s
Ah, because LazyPhoto is a View(); it take transform and camera params.
In the demo, where is this camera icon coming from (the html?) what I use to manually rotate the cube? And I am just copy/pasting your Cube view code, with its
rect
, but my drawing is a diamond:
My hack.kt:
hack.kt
In your Cube view, there is no init{}, so that may be interfereing. But I could only set the camera and transform on the image-view in the init{}. I could not pass a params. Will keep at it.
I'm lost, trying to fit a drawing of a diamond into a rectangle. So I don't expect you to help me this with until I have better questions.
And you pointed me to the cards... not the cube.
n
The camera and transform are properties you set after creating the view.
s
Hi Nicklolas, There is a form constructor that takes five fields. I need to pass 7 fields into a form constructor. I created a list:
[EDIT Remove some noise.] This is the constructor I need to use:
Copy code
public 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.
n
You can just invoke the form builder like in the examples in the doc, just with more fields. The issue is the lambda for your consumption of each field will give you an array instead of individual values:
Copy code
val 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.
s
That was noisy... I was already almost there before detouring to the constructor that takes a
vararg
param. It works now. Thanks again for your help.
n
Another thing to consider is using form fields to group some of your fields. This might allow you to avoid the extension/casting route as well.
s
I am working on it... Getting there... But jumping back to setting the "artists'" color/logo preferences (finally;-). And some day will ask you how to center text in a button... And about that IR compiler, which I have to ignore for now, due to project's inability to reference "nacular" anything when using IR compiler.
n
You might consider creating a custom field for password/verify-password that returns a
String
, but has 2 text fields that do validation to make sure they are the same.
s
Added your remark as comment in form class... Tnx.
n
By the way. Please drop a star for Doodle if you haven’t already, and are finding it useful so far.
s
Done. I'd give 256 stars if I could.
I'm struggling with the form dsl... Trying to use
fun <T> formFields()
. I have a data class:
Copy code
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 ?
The forms-as-fields doc shows me how to declare the form within the parent form, but my problem(s) are due to declaring the child form in another class file, and I don't know what the correct syntax is -- among other things I don't possibly know ;-)
n
You should be able to define the sub form as a variable some place and then add it to your main form:
Copy code
val 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.
Copy code
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?
s
Well, I can't even define
val subForm = form { this( ...
in a file:
"it"s name and age fields cannot be referenced.
There is another use case (Reset Password), which is why I want to implement the sub-form in a separate file, instead of within the main-form.
n
Sorry, no computer to make sure things compile 😅
You will need to specify the type of the sub form since it is a generic. The snippet i shared is from the docs where they form was being used to create a
Person
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.
That snippet i shared should work if you add
Copy code
data 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.
sorry, just realized you did define the Person data class. hmmm.
Kotlin might not understand the generic type. Can you add the type as follows?
Copy code
var sub = form<Person> {
    …
}
s
I realized I probably just had to set the form<Type> after I had to leave for a bit. It works. Thanks again!
Copy code
var 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)
        }
    }
}
n
Didn’t see your question about the custom password field. Take a look at this doc. It shows that you can create a new
field
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.
Copy code
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.
s
I should be checking for pwd/confirm-pwd match in the field definition, analogous to the custom-control example, and NOT inside the data class itself?
I will take a slower, longer look at custom fields... Right now I'm trying to build a
CPF
field, and probably need to make it a custom field. A CPF is equivelant to a US Social Security Number, in format
Copy code
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 ?
n
Yes. That way your form doesn’t become valid until they match. And you can have your custom field show help text/some visual to indicate this.
A subform is a really simple way to get your CPF to work. But it might be nicer (in the long run) to create a custom field that handles the CPF format (i.e. makes the 4 textfields feel like a single input as you type by automatically changing focus).
s
I just want to get it done and move on 🤠 But I want to get it right first, then move on... So I gotta take it easy, slow down and 🤔 You're being extremely helpful.
How do I use custom-field (customCpfField) in a form with other conventionally declared fields?
Copy code
val customCpfField = cpfField(StyledText("CPF", config.formTextFieldFont, Black.paint))
Copy code
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")
            )
        },
Maybe you still do not have a compiler 😉
n
Same as any other field.
Copy code
val form = Form { this(
    true to switchField("Wasn't that "..boldFont("easy").."?"),
    onInvalid = {}
) { bool: Boolean ->
    println("Form valid: $bool")
} }
s
Wasn't that easy he says... Well, this noob did not find it so, but I am stubborn. I have a custom CPF field (like a US SSN). It looks like this:
I spent a lot of time trying to figure out how to get the help text to work in the custom field, like it does for most of the other fields, like the first field I place in the main form. But as you can see, I could not find a way to use a
fun <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"?
n
You should be able to take a
LabelConfig
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`.
Copy code
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
        }
}
Copy code
+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.
s
> 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?
n
You need a
LabeledConfig
, 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
Copy code
labelConfig.helper.styledText = …
Inside validateCpf.
s
So much simpler than I thought... It works. I'm messing around with clearing & setting the error msg as text changes & focus changes, but it it is working almost exactly how I want inside validateCpf() -- though I need to test & tweak more before I go writing more of these custom fields. Thanks!
cpf-subfield-validation.webm
Thanks for helping me with my first custom control (class).
n
Your welcome!
s
Hi Nick, I got around the IR problem by creating a new KMP project Intellij and moving my code into it. I guess I got so hung up on webpack config + source map generation I screwed up IR compatibility. Cut code size by about 1.5x.
My new repo is private. I want to keep it that way, but also want you to be able to see it. I was going to take the liberty of making you a collaborator, but there is no way to just give read-only access. I have built some trust with you, but don't want to be stupid. I'll wait & think about that, and hope you can give your opinion about this issue.
n
Hey Stan. Really glad to hear you resolved the IR issue. It really does make a huge difference in build size. I’m always looking for real world uses of Doodle so I can learn what’s working well and what’s not. So it’s been helpful to collaborate with you so far; and I’d be happy to continue helping when I have the time.
s
I invited you, 2x. I'm still waiting for good photos of Natty's bling ( otherwise, the shop will have to show burgers on the left, fries on the right, full platter in the center... or maybe pizzas & toppings 😉 ) But Natty will deliver. I've been spending time re-organizing code, re-doing w/ IR, and learning how to do gradle plugins in kotlin, which I need for tweaking the distribution layout, and probably more. Not to mention months figureing out how to build native microservices with Quarkus/Java, and deploy everything on Talos/Kubernetes. Almost getting to a point where I can start hooking up bits of doodle to horizontally & vertically scalable backend -- also wip, of course. Thanks again!
Hi Nick, I am learning about Selenium, for UI / end-to-end testing. I need to access html 'elementById()' values in test scripts. Is there a way to programmatically set element ID's for fields and buttons in doodle?
Is <input placeholder .../> what I need to use, instead? I don't even see any element.id in the rendered html.
n
hey Stan. the generated HTML is an internal detail of Doodle, so i wouldn’t recommend depending on its structure for your tests. It’s better to test at the View level itself. This would also have the added benefit of not requiring any headless browser. my recommendation is to use mocks and fakes for various parts of the system to do your validation. things like render validation can be verified using Canvas mocks if needed. unfortunately i don’t think there’s a good mock lib for JS. Mockk is awesome, but is JVM only. this means you either hand roll fakes, or make your project multi platform and have your app code in common. then you can write your tests for the jvm. i highly recommend this by the way, since it makes test execution very fast. this is the approach i use for the Calculator tutorial.
what kinds of tests are you hoping to write?
s
Simple at first: just a form-sumission test to avoid having to do it manually. I took a look at the selenium browser extension, but balked at the "give us all the permissions to everything you do in the browser".
Now I am taking a closer look at using Java/Selenium scripts, instead of recording browser activity, and playing it back. For that, my tests need to
getElementById('xyz-field')
to do anything: input text, push buttons, etc...
Eventually, I want to be able to do comprehensive end-to-end testing from UI to back-end services.
n
yeah, you’ll likely be better off rolling it manually. i’d test the custom form fields using unit tests to make sure they do the right validation and keep the
state
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.
s
Regarding you remark about testing at the view level... I have some homework to do before I know anything. But, project is multi-platform, and all my code is on
commonMain
.
n
i noticed that about it being multiplatform. this will make things much easier.
s
I have started creating some unit tests for field formatting/validation: https://github.com/ghubstan/project-natty/tree/main/shop/src/commonTest/kotlin/io/dongxi/page/panel/form
I am starting to organizing things a bit, with the custom controls + format/validation: RegistrationForm
Took me quite awhile to go from completely lost to where I am now, doodle-wise.
Thanks for that: "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." I won't waste any more time on selenium and/or alternatives.
Can I use
mockk
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.
n
i think you’ll need to put your
mockk
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.
s
OK, so, I can put tests in
jvmTest
, 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?
The muiltiplatform project's
jvmMain
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-frontend
n
you’d only be able to do that if jvmTest passes a jvm impl (of a commonMain interface/abstract, that calls the backend) into a commonMain class. in this way, commonMain class can call a real impl and connect with the backend in the test.
s
I want to use a real font loader in a test, not a mockk'd font load... Is there a way to instantiate one in jvmTest?
val fontLoader: FontLoader = _FontModule_.???
Likewise, for other modules, conventionally installed in an App? I want real modules, without having to bootstrap a test App.
n
this is really tricky since those impls depend on Browser/or desktop internals. So you can’t create them without being in those environments. my advice is to try and break your testing down so you don’t actually need the concrete classes for stuff outside your code. then you can mock those things and just test your code. this works great if your code uses dependency injection so you can provide mocks. then you can prove your code works as expected given a specific understanding of the APIs you use. this feels like a less powerful way to test, but it actually gives you a lot more precision, let’s you focus on validating your code only and avoids the complexities of trying to rig real dependencies for a test setup, which is usually hard to do and maintain. this

video

explains a bit more of this idea.
s
I was getting pretty discouraged 😉 I put a lot of work into E2E testing an API I built, exposed in gRPC endpoints, in last project I worked on. It really helped in terms building trust that it worked, and it sped up the building/testing of new endpoints. Lately, I've been concerned about having to manually test UI bits -- a huge time-suck. But browser-land is indeed a very different beast. Glad I pestered you about this. I will take your advice. Glad I asked.
Hi Nick, I am starting to understand basic mockk usage. I want to test the simplest custom control I've made: EmailControl . I need to create a LabeledConfig in my test case by calling your <https://github.com/nacular/doodle/blob/649c60e2cb79fb48cd2351b1f281a9acb3652a03/Controls/src/commonMain/kotlin/io/nacular/doodle/controls/form/FormControls.kt#L1603%7Cpublic fun <T> labeled()> . How do I do this in a test case? In my RegistrationForm , I just pass labelConfig=this:
emailControl.emailField(labelConfig = this)
. But I need to pass an instance I create in the test case, not
this
.
n
hey Stan. you should be able to just create a mockk of the LabelConfig and pass that to your EmailControl directly in a test. you can mockk out the
FieldInfo
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.
Copy code
@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 }
}
s
[EDIT / REMOVE NOISE] I am a little lost about the
// validate view
bits, but I think I am going to be able to figure it out in EmailControlTest.kt
And I check in broken code, where I attempt to update state and notify my field. But,
lateinit property captured has not been initialized
Can you help de-confuse me about this mockk feature?
n
you should be casting to Container/ContainerBuilder and not TextField; my bad. you're getting that error b/c you never add a listener to
stateChanged
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:
Copy code
if (stateListener.isCaptured) {
    // ...
}
Mockk lets you capture inputs in your
every
blocks. this is what i showed with this part of the code:
Copy 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.
you can also take a look at the tests i have for the built-in controls. they do a lot of what you might want to for your validation.
s
I began to understand my test case needs to put my custom field in a Form, like you do here. But can you clarify: (1) "you're getting that error b/c you never add a listener to
stateChanged
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.
n
yes. your changes that register for calls from
stateChanged
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
).
you should just use a
Form
directly in the test 😅
s
Ugh, took too long and made a mess to grok &amp; clean up, but I think I get it now. Any advice? Nothing in the test invoke's the controls stateChanged += impl. Is that OK? textChanged seems to be called at the right time.
n
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.
i suggest testing the default/initial value cases in separate test methods where you embed the field in a
Form
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.
s
My control implements stateChanged:
stateChanged += *{*
_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.
Test1 works, but Test2 is telling me I am doing something very wrong. Specifically, here.
n
the
stateListener.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.
Copy code
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 }
also, you don't need to use the Form in the tests where you use the FieldInfo. that's b/c the Form will create it's own internal FieldInfo and you'll have your field invoked twice. just use the FieldInfo for the tests where you want to manipulate the returned Container/Textfield. only use the Form for the tests of initial values (default, valid, invalid).
Copy code
@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(     ) }
}
something like this. otherwise, you'll need to use the
FieldInfo
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()
.
s
I'm trying your @Test fun `ensure defaults to blank`() . But
_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.
n
haha. this is actually good. forgot this is an email field. so your test should do the opposite of what i sent. it should make sure valid is never called in this case and that invalid is called.
s
I did check that invalid is called, but it is not. Checking again...
n
so maybe your field isn’t calling it. which it should do if the
initial
is not a valid email.
s
It is not. But when an email field is displayed, it is blank; that's OK(?) cuz there is no error state present -- yet.
n
i think Doodle fields default to Invalid, so might work as expected. but i recommend setting the field to Valid or Invalid upon creation to avoid making that assumption.
being invalid will prevent the form from accidentally including a blank email. unless your field allows for that.
do you want email to be optional? if so, you can set the state to valid for blank as well.
s
The form submit button will remain disabled until all the fields have valid values, incl email field. But when the form is 1st displayed, all fields are blank, and there is no invalid state until edits are made .
Email is not optional.
nothing is
I'm new at this, so I deferr to your advice on these matters. (Except any form field being optional 😉
I confirm "think Doodle fields default to Invalid"
n
then you should set the field to Invalid if the initial value is blank/not a parsable email. you should also do the same as the TextField value changes. then your test should verify that a form with no value bound to the email is invalid. and forms with ill formed emails are also invalid.
s
RE: "then you should set the field to Invalid if the initial value is blank" ... The field's state is already Form.Invalid() on creation.
n
but you should set it to valid if the initial value is a valid email.
s
But in your blank initial value test, the inital value is invalid, and the
_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?)
n
should be
verify(exactly=0){ onValid(any()) }
. no calls to onValid.
but 1 call to onInvalid.
s
There are zero calls to onInvalid, and no way to check field itself for validity.
Ignore me for a bit... I think I'm on to something (using mockked fieldInfo after those verify calls).
All this back and forth is reassuring me a bit that I'm on the right track. At least the 1st test case works now (blank field). What I need to do is create a mockk'd fieldInfo after the 0-verifies (onValid, onInvalid), and use my EmailValidator to set the initial FieldInfo state when the FieldInfo is mockk'd. Then I can check fieldInfo state and textField.text here. Do you think this test is done right?
n
i suggest nestling your tests into really small methods to isolate a single case.
this code doesn’t look like the right approach. your field should do all the validation internally and you should observe the effects.
s
The EmailControl validates in textChanged and focusChanged. This code is for creating the mockk<FieldInfo>, as in your earlier examples. I don't know of any other way to get a Form's FieldInfo object.
n
i see, you're reusing the internal validator to decide if the initial value is valid or not.
consider just passing a state explicitly instead of a string that is validated to avoid coupling w/ the validator.
otherwise, the test looks reasonable. but you could do it more simply since this is just checking initial conditions. you could use the Form for these tests:
Copy code
@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(     ) }
}
this is how you might write some of the tests to see if the initial value results in the text field having the right initial text:
Copy code
@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 }
you could then do tests that check when user input changes and if the field changes state accordingly. this tests for whether valid input results in the right Valid state.
Copy code
@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>") }
}
my examples might have bugs, since i'm not testing them against your code. but hopefully they illustrate the approach
the
relaxed=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.
by the way. 0.9.2 is releasing soon and it will add a new
purpose
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.
s
I owe you lots. I need to tweak your example a bit to create real, un-mockk'd Forms with real LabeledConfigs because the validator throws exceptions with specific error messages (accessed & test-asserted via LabeledConfig.help), but I got the idea. You've given me so much guidance, I gotta be real thick to not get this right tomorrow. Gratitude! G'night.
n
you’re welcome. pls do try out the 0.9.2 snapshot when you get some time. that would be a big help to me. i’m hoping to release next week.
s
It might not be exactly as you'd want it, but it is better. What I did to simplify things was cache the LabeledConfig, FieldInfo, and TextField in the custom controller. I really do need the real objects to test the validation error msgs, and it is simpler for me cuz I'm so new to mockk. But I don't know if cacheing those objects in the controller is dangerous, security-wise. I just upgraded doodle, tests work, and a quick pass through my UI prototype seems OK. Thanks again for all the help!
n
looks a lot clearer! one thing to consider is creating a class for your custom field that implements
FieldVisualizer
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.
s
Why not regex/your email validator? A comprehensive email regex is very complex and impossible for a mortal like me to understand. https://stackoverflow.com/questions/201323/how-can-i-validate-an-email-address-using-a-regular-expression/201378#201378 https://www.ex-parrot.com/~pdw/Mail-RFC822-Address.html I also need to allow for portuguese characters in the address. And the real validation comes in the completed send-email-with-token & process response. The UI validation just needs to be "good enough for now". I will get back to you on the other issues after Sunday 😉
n
agreed on the validation complexity and shooting for “good enough”. so not suggesting you try to rely on a regex for the validation. just thought you could pass a permissive regex (say not empty string) along with your validator and make it work. mostly just curious to see if there were some other limitation i didn’t see.
s
I will try implementing interface
FieldVisualizer
. I want to get this right before I go replicating crap 50x.
n
good idea. i’d say get it working then refine at your leisure.
s
I'm back 😳 My custom EmailControl implements
FieldVisualizer<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!
n
oh, the suggestion was just to keep the internal fields more private. so now your tests could depend on the
EmailControl
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.
s
I like the idea. My very happy path would be to encapsulate these controls in a library to be used by various apps: jewelry-shop, food-delivery-app, etc. But I'm not sure how to get there by having EmailControl implement FieldVisualizer<String>, or even if that is what you're suggesting. Seems not urgent now.
Good afternoon, Nick. I have been tinkering around quite a bit with my 1st form (registration), spending ridiculous amounts of time clicking around the pwd and pwd-confirm fields to fool the form into thinking it is valid (to enable the submit button), when it really is not: • The password is valid • The confirmation field does not match because I would moved focus around different fields and do edits to make pwd & confirm not match anymore. I hacked around the problem in the containing Form, here. Is this not a good idea? Have you got pwd and pwd-confirm down perfectly, so you would never have to do this? Here is my UserPasswordControl. If at all curious, my UserPasswordControlTest, and simple PasswordValidatorTest.
n
hey Stan. are you saying that you're having issues where the form becomes valid when it shouldn't?
s
Yes, and it must be my bug. The nature of the problem is this: I have a custom pwd + pwd-confirm control, where the primary pwd field is validated, and the pwd-confirm field is only checked that it matches the valid pwd-field. I have not nailed down the exact sequence of events that causes it, other than generalizing it as "trying to break it by clicking in and out of the pwd fields, while editing the confirm field to make them not match -- after they were matching". So, the custom control's "field" state does depend on a match, and there are situations where the confirm fld is tweaked (mismatched) and the valid pwd field stays valid ("fooling" the form).
I am avoiding setting the VALID primary (contol) field state to INVALID where there is a mismatch. Doing that created confusing behaiviors, but I might want to re-visit that.
n
i only had time to do a quick check, so may not be the root cause. but it seems like you validate the primary and secondary fields when they lose focus (which makes sense). but you set the overall field state to Valid if the primary passes all your validation checks. shouldn't you only set it to valid if the checks are good AND it matches the contents of the secondary field? here's a quick sample of how you could achieve something similar to what you want using a nested Form with two `textField`s. this only sets the field state to Valid when both its fields are valid and they both match. that way a form using it only gets a valid
String
out when it should.
Copy code
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)
        }
    }
}
s
Haven't gotten to this yet, but thanks 👍
This seems a very good solution -- wish I understood the benefit of nested forms before now 🙂 One question: how do I make the nested form not focusable?
n
you should be able to set
focusable = false
in the apply block.
s
.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.
G'night.
n
forms take the same `layout`s as `container`s. so you should be able to create one that arranges the list of children however you want based on the size of the form (the
container
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).
s
Ai ai... Kotlin syntax is still befuddling me at times... Regarding my attempt to use a horizontal (not vertical) password form layout: How do I pass in a
NamedConfig
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. 😳
n
the lambda you pass to the
labeled
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.
s
I could not get the confirm-pwd to display (horizontally) using +labeled, but +textField works just fine -- less noisy, less code, and using
textField.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.
Hope I am not being rude, and seem like I am thick and expecting too much from you. (This thread contains 390+ replies.)
n
happy to help on the occasions i have free time. i’ll take a look at the textfield issue when i’m back in town.
the mask value not affecting the behavior is just a bug 🤦. it should behave just like setting the
purpose
to
Password
. will be fixed in the next version. what issues do you see when using
purpose
though?