Closed minmon98 closed 3 years ago
Ohh sorry @Tiagoperes for my mistakes. I created a custom action to show a date picker on native iOS. when I pick a date, I don't know how to trigger to beagle screen know the data
I think there's a simpler approach to this. Instead of opening a DatePicker via a custom action, you should create a new component.
This component is a Date Input. It shows the date value of the input, or a placeholder in the case no value has been set. when the user presses the input, a DatePicker is shown. When the user selects a date in the DatePicker, the value of the input changes to the selected date.
This custom component (DateInput) must have at least these two attributes:
value
: the value of the inputonChange
: the actions to execute when the value of the input changesIn your backend, the view which uses a DatePicker would be similar to this:
{
"_beagleComponent_": "beagle:container",
"context": {
"id": "form",
"value": {
"date": "2021-04-12"
}
},
"children": [
{
"_beagleComponent_": "custom:dateInput",
"value": "@{form.date}",
"onChange": [
{
"_beagleAction_": "beagle:setContext",
"contextId": "form",
"path": "date",
"value": "@{onChange.value}"
}
]
}
]
}
In summary, your custom component DateInput will be like any other input, but instead of opening a keyboard to type the value, it will open a DatePicker. The date value must be serialized somehow. Here I assumed the format year-month-day
.
The view in the example will render a DateInput that, when changed, will alter the value in the context. You can use this context for whatever you want after that. Note that I used form.date
, I did this because you'll probably want more fields in this form. If this is not the case, you can declare the context as a string instead of a <key, value> map.
But, there's one thing missing: how can you create a component that interacts with actions passed by Beagle. In other words, how do you implement the onChange
event of the component?
This is our bad, we don't say how to do this in our documentation. We'll fix this asap. In any case I asked @lucasaraujo, from the iOS team, to help you with this. He will create an example of the DateInput component and as soon as it's ready, he'll post it here.
Here is a sample to help you implement an input component.
First let's define our component, in this case a DatePicker:
public struct DatePicker: ServerDrivenComponent {
public var date: Expression<String>? // [1] It's an Expression to allow us to read and set the value using a Context.
public var onChange: [Action]? // [2] This Actions allow us to respond to changes in the component.
public func toView(renderer: BeagleRenderer) -> UIView {
// the Expression binding and Action execution will be handled in the class DatePickerView.
let datePicker = DatePickerView(self, renderer: renderer)
return datePicker
}
}
extension DatePicker {
enum CodingKeys: String, CodingKey {
case date
case onChange
}
/// The decoder implementation is required because `Action` is a protocol, so we have to provide it.
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
date = try container.decodeIfPresent(Expression<String>.self, forKey: .date)
// Beagle provide `KeyedDecodingContainer` extension methods to decode registered actions and components.
onChange = try container.decodeIfPresent(forKey: .onChange)
}
}
The implementation will use the native iOS DatePicker view:
class DatePickerView: UIDatePicker {
let component: DatePicker
weak var controller: BeagleController?
private static var formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withFullDate
formatter.timeZone = TimeZone.autoupdatingCurrent
return formatter
}()
private var dateString: String? {
get { Self.formatter.string(from: date) }
set {
guard let string = newValue,
let date = Self.formatter.date(from: string) else { return }
self.date = date
}
}
init(_ component: DatePicker, renderer: BeagleRenderer) {
self.component = component
self.controller = renderer.controller
super.init(frame: .zero)
datePickerMode = .date
preferredDatePickerStyle = .compact
// [1] Use the renderer method `observe` to bind the expressions
renderer.observe(component.date, andUpdate: \.dateString, in: self)
addTarget(self, action: #selector(valueChanged), for: .valueChanged)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func valueChanged() {
guard let dateString = dateString else { return }
// [2] Here the actions are executed and the new value is exposed through the implicit context `onChange`
controller?.execute(actions: component.onChange, with: "onChange", and: .string(dateString), origin: self)
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
return super.sizeThatFits(.zero)
}
}
Finally we can register and use the component:
Beagle.dependencies.decoder.register(component: DatePicker.self)
// Using the swift DSL
let viewController = BeagleScreenViewController(
Container(context: Context(id: "ctx", value: ["date": "2020-01-31"])) {
DatePicker(
date: "@{ctx.date}",
onChange: [SetContext(contextId: "ctx", path: "date", value: "@{onChange}")]
)
Text("The date is @{ctx.date}")
}
)
Here is the above sample in json:
{
"_beagleComponent_": "beagle:container",
"context": {
"id": "ctx",
"value": {
"date": "2020-01-31"
}
},
"children": [
{
"_beagleComponent_": "custom:datePicker",
"date": "@{ctx.date}",
"onChange": [
{
"_beagleAction_": "beagle:setContext",
"contextId": "ctx",
"path": "date",
"value": "@{onChange}"
}
]
},
{
"_beagleComponent_": "beagle:text",
"text": "The date is @{ctx.date}"
}
]
}
Woww thanks a lot for your support @Tiagoperes , @lucasaraujo . It's exactly the response I'm waiting for
Here is a sample to help you implement an input component.
First let's define our component, in this case a DatePicker:
public struct DatePicker: ServerDrivenComponent { public var date: Expression<String>? // [1] It's an Expression to allow us to read and set the value using a Context. public var onChange: [Action]? // [2] This Actions allow us to respond to changes in the component. public func toView(renderer: BeagleRenderer) -> UIView { // the Expression binding and Action execution will be handled in the class DatePickerView. let datePicker = DatePickerView(self, renderer: renderer) return datePicker } } extension DatePicker { enum CodingKeys: String, CodingKey { case date case onChange } /// The decoder implementation is required because `Action` is a protocol, so we have to provide it. public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) date = try container.decodeIfPresent(Expression<String>.self, forKey: .date) // Beagle provide `KeyedDecodingContainer` extension methods to decode registered actions and components. onChange = try container.decodeIfPresent(forKey: .onChange) } }
The implementation will use the native iOS DatePicker view:
class DatePickerView: UIDatePicker { let component: DatePicker weak var controller: BeagleController? private static var formatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withFullDate formatter.timeZone = TimeZone.autoupdatingCurrent return formatter }() private var dateString: String? { get { Self.formatter.string(from: date) } set { guard let string = newValue, let date = Self.formatter.date(from: string) else { return } self.date = date } } init(_ component: DatePicker, renderer: BeagleRenderer) { self.component = component self.controller = renderer.controller super.init(frame: .zero) datePickerMode = .date preferredDatePickerStyle = .compact // [1] Use the renderer method `observe` to bind the expressions renderer.observe(component.date, andUpdate: \.dateString, in: self) addTarget(self, action: #selector(valueChanged), for: .valueChanged) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func valueChanged() { guard let dateString = dateString else { return } // [2] Here the actions are executed and the new value is exposed through the implicit context `onChange` controller?.execute(actions: component.onChange, with: "onChange", and: .string(dateString), origin: self) } override func sizeThatFits(_ size: CGSize) -> CGSize { return super.sizeThatFits(.zero) } }
Finally we can register and use the component:
Beagle.dependencies.decoder.register(component: DatePicker.self) // Using the swift DSL let viewController = BeagleScreenViewController( Container(context: Context(id: "ctx", value: ["date": "2020-01-31"])) { DatePicker( date: "@{ctx.date}", onChange: [SetContext(contextId: "ctx", path: "date", value: "@{onChange}")] ) Text("The date is @{ctx.date}") } )
Here is the above sample in json:
{ "_beagleComponent_": "beagle:container", "context": { "id": "ctx", "value": { "date": "2020-01-31" } }, "children": [ { "_beagleComponent_": "custom:datePicker", "date": "@{ctx.date}", "onChange": [ { "_beagleAction_": "beagle:setContext", "contextId": "ctx", "path": "date", "value": "@{onChange}" } ] }, { "_beagleComponent_": "custom:text", "text": "The date is @{ctx.date}" } ] }
Sorry for any inconvenience. I'm working on the same project with @minmon98 and can you show me how to implement it on Android? Thank you in advance! @Tiagoperes @lucasaraujo
@hoangnv11 sure, we can also implement this example for Android. @luisoliveirazup will help you with this.
We are going to use these examples to update our documentation. Thanks for bringing this issue to our attention!
@hoangnv11 sure, we can also implement this example for Android. @luisoliveirazup will help you with this.
We are going to use these examples to update our documentation. Thanks for bringing this issue to our attention!
Oh thank you guys. I'm looking forward to your example. I believe that this will help a lot of developers in our Beagle community :D
Here is a sample to help you implement an android component.
First let's define our component, in this case a DatePicker:
@RegisterWidget
class DatePicker(
val date: Bind<String>,
val onChange: List<Action>
) : WidgetView() {
override fun buildView(rootView: RootView) = DatePickerComponent(rootView.getContext()).apply {
observeBindChanges(rootView, this, date) { text ->
text?.let { setText(it) }
}
dateSetListener = object : DateSetListener {
override fun onDateSet(value: String) {
this@DatePicker.handleEvent(
rootView,
this@apply,
onChange,
ContextData(
id = "onChange",
value = value
)
)
}
}
}
}
The implementation will use the native Android DatePicker view:
class DatePickerComponent constructor(
context: Context
) : TextView(context), DatePickerDialog.OnDateSetListener {
private val myCalendar: Calendar = Calendar.getInstance()
var dateSetListener: DateSetListener? = null
init {
this.setOnClickListener {
DatePickerDialog(context, this, myCalendar
.get(Calendar.YEAR), myCalendar.get(Calendar.MONTH),
myCalendar.get(Calendar.DAY_OF_MONTH)).show()
}
}
fun setText(text: String) {
this.text = text
}
private fun Date.formatDate(): String{
val myFormat = "yyyy-MM-dd"
val simpleDateFormat = SimpleDateFormat(myFormat, Locale.US)
return simpleDateFormat.format(this).toString()
}
override fun onDateSet(view: DatePicker?, year: Int, month: Int, dayOfMonth: Int) {
myCalendar[Calendar.YEAR] = year
myCalendar[Calendar.MONTH] = month
myCalendar[Calendar.DAY_OF_MONTH] = dayOfMonth
dateSetListener?.onDateSet(myCalendar.time.formatDate())
}
}
interface DateSetListener{
fun onDateSet(value: String)
}
Here is a sample to help you implement an android component.
First let's define our component, in this case a DatePicker:
@RegisterWidget class DatePicker( val date: Bind<String>, val onChange: List<Action> ) : WidgetView() { override fun buildView(rootView: RootView) = DatePickerComponent(rootView.getContext()).apply { observeBindChanges(rootView, this, date) { text -> text?.let { setText(it) } } dateSetListener = object : DateSetListener { override fun onDateSet(value: String) { this@DatePicker.handleEvent( rootView, this@apply, onChange, ContextData( id = "onChange", value = value ) ) } } } }
The implementation will use the native Android DatePicker view:
class DatePickerComponent constructor( context: Context ) : TextView(context), DatePickerDialog.OnDateSetListener { private val myCalendar: Calendar = Calendar.getInstance() var dateSetListener: DateSetListener? = null init { this.setOnClickListener { DatePickerDialog(context, this, myCalendar .get(Calendar.YEAR), myCalendar.get(Calendar.MONTH), myCalendar.get(Calendar.DAY_OF_MONTH)).show() } } fun setText(text: String) { this.text = text } private fun Date.formatDate(): String{ val myFormat = "yyyy-MM-dd" val simpleDateFormat = SimpleDateFormat(myFormat, Locale.US) return simpleDateFormat.format(this).toString() } override fun onDateSet(view: DatePicker?, year: Int, month: Int, dayOfMonth: Int) { myCalendar[Calendar.YEAR] = year myCalendar[Calendar.MONTH] = month myCalendar[Calendar.DAY_OF_MONTH] = dayOfMonth dateSetListener?.onDateSet(myCalendar.time.formatDate()) } } interface DateSetListener{ fun onDateSet(value: String) }
Thank you @luisoliveirazup. It works like a charm :D
Hello @minmon98. Can you please elaborate the question? Maybe give us some examples of what you're trying to achieve.