-
Notifications
You must be signed in to change notification settings - Fork 6
/
control_base.go
1998 lines (1715 loc) · 70.8 KB
/
control_base.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package page
import (
"bytes"
"context"
"fmt"
"github.com/goradd/gengen/pkg/maps"
"github.com/goradd/goradd/pkg/base"
"github.com/goradd/goradd/pkg/config"
"github.com/goradd/goradd/pkg/html"
"github.com/goradd/goradd/pkg/i18n"
"github.com/goradd/goradd/pkg/javascript"
"github.com/goradd/goradd/pkg/log"
"github.com/goradd/goradd/pkg/orm/query"
"github.com/goradd/goradd/pkg/page/action"
buf2 "github.com/goradd/goradd/pkg/pool"
"github.com/goradd/goradd/pkg/session"
"github.com/goradd/goradd/pkg/stringmap"
"github.com/goradd/goradd/pkg/watcher"
gohtml "html"
"reflect"
)
const PrivateActionBase = 1000
const sessionControlStates string = "goradd.controlStates"
const sessionControlTypeState string = "goradd.controlType"
const RequiredErrorMessage string = "A value is required"
// ValidationType is used by active controls, like buttons, to determine what other items on the form will get validated
// when the button is pressed. You can set the ValidationType for a control, but you can also set it for individual events
// and override the control's validation setting.
type ValidationType int
const (
// ValidateDefault is used by events to indicate they are not overriding a control validation. You should not need to use this.
ValidateDefault ValidationType = iota
// ValidateNone indicates the control will not validate the form
ValidateNone
// ValidateForm is the default validation for buttons, and indicates the entire form and all controls will validate.
ValidateForm
// ValidateSiblingsAndChildren will validate the current control, and all siblings of the control and all
// children of the siblings and current control.
ValidateSiblingsAndChildren
// ValidateSiblingOnly will validate only the siblings of the current control, but not any child controls.
ValidateSiblingsOnly
// ValidateChildrenOnly will validate only the children of the current control.
ValidateChildrenOnly
// ValidateContainer will use the validation setting of a parent control with ValidateSiblingsAndChildren, ValidateSiblingsOnly,
// ValidateChildrenOnly, or ValidateTargetsOnly as the stopping point for validation.
ValidateContainer
// ValidateTargetsOnly will only validate the specified targets
ValidateTargetsOnly
)
// ValidationState is used internally by the framework to determine how the control's wrapper handles drawing validation error
// messages. Different wrappers use it to set classes or attributes of the error message or the overall control.
type ValidationState int
const (
// ValidationWaiting is the default for controls that accept validation. It means that the control expects to be validated,
// but has not yet been validated. Wrappers should save a spot for the error message of this control so that if
// an error appears, it will not change the layout of the form.
ValidationWaiting ValidationState = iota
// ValidationNever indicates that the control will never fail validation. Essentially it indicates that the wrapper does not
// need to save a spot for an error message for this control.
ValidationNever
// ValidationValid indicates the control has been validated. This state gets entered if some control on the form has failed validation, but
// this control passed validation. You can choose to display a special message, or a special color, etc., to
// indicate to the user that this is not the source of the validation problem, or do nothing.
ValidationValid
// ValidationInvalid indicates the control has failed validation, and the wrapper should somehow call that out to the user. The error message
// should be displayed at a minimum, but likely other things should happen as well, like a special color, and
// aria attributes should be set.
ValidationInvalid
)
// ControlTemplateFunc is the type of function control templates should create
type ControlTemplateFunc func(ctx context.Context, control ControlI, buffer *bytes.Buffer) error
// ControlWrapperFunc is a template function that specifies how wrappers will draw
type ControlWrapperFunc func(ctx context.Context, control ControlI, ctrl string, buffer *bytes.Buffer)
// DefaultCheckboxLabelDrawingMode is a setting used by checkboxes and radio buttons to default how they draw labels.
// Some CSS framworks are very picky about whether checkbox labels wrap the control, or sit next to the control,
// and whether the label is before or after the control
var DefaultCheckboxLabelDrawingMode = html.LabelAfter
// The DataConnector moves data between the control and the database model. It is a thin view-model controller
// that can be customized on a per-control basis.
type DataConnector interface {
// Refresh reads from the model, and puts it into the control
Refresh(i ControlI, model interface{})
// Update reads data from the control, and puts it into the model
Update(i ControlI, model interface{})
}
// DataLoader is an optional interface that DataConnectors can use if they need to load data from the database
// to present a choice of items to the user to select from. The Load method will be called whenever the entire control
// gets redrawn.
type DataLoader interface {
Load(ctx context.Context) []interface{}
}
// ControlI is the interface that all controls must support. The functions are implemented by the
// ControlBase methods. See the ControlBase method implementation for a description of each method.
type ControlI interface {
ID() string
control() *ControlBase
DrawI
// Drawing support
DrawTag(context.Context) string
DrawInnerHtml(context.Context, *bytes.Buffer) error
DrawTemplate(context.Context, *bytes.Buffer) error
PreRender(context.Context, *bytes.Buffer) error
PostRender(context.Context, *bytes.Buffer) error
ShouldAutoRender() bool
SetShouldAutoRender(bool)
DrawAjax(ctx context.Context, response *Response) error
DrawChildren(ctx context.Context, buf *bytes.Buffer) error
DrawText(ctx context.Context, buf *bytes.Buffer)
// Hierarchy functions
Parent() ControlI
Children() []ControlI
SetParent(parent ControlI)
Remove()
RemoveChild(id string)
RemoveChildren()
Page() *Page
ParentForm() FormI
Child(string) ControlI
RangeAllChildren(func(ControlI))
RangeSelfAndAllChildren(func(ControlI))
// hmtl and css
SetAttribute(name string, val interface{}) ControlI
Attribute(string) string
HasAttribute(string) bool
ProcessAttributeString(s string) ControlI
DrawingAttributes(context.Context) html.Attributes
AddClass(class string) ControlI
RemoveClass(class string) ControlI
HasClass(class string) bool
SetStyles(html.Style)
SetStyle(name string, value string) ControlI
SetWidthStyle(w interface{}) ControlI
SetHeightStyle(w interface{}) ControlI
Attributes() html.Attributes
SetDisplay(d string) ControlI
SetDisabled(d bool)
IsDisabled() bool
PutCustomScript(ctx context.Context, response *Response)
TextIsLabel() bool
Text() string
SetText(t string) ControlI
ValidationMessage() string
SetValidationError(e string)
ResetValidation()
WasRendered() bool
IsRendering() bool
IsVisible() bool
SetVisible(bool)
IsOnPage() bool
Refresh()
NeedsRefresh() bool
Action(context.Context, ActionParams)
PrivateAction(context.Context, ActionParams)
SetActionValue(interface{}) ControlI
ActionValue() interface{}
On(e *Event, a action.ActionI) ControlI
Off()
WrapEvent(eventName string, selector string, eventJs string, options map[string]interface{}) string
HasServerAction(eventName string) bool
HasCallbackAction(eventName string) bool
// UpdateFormValues is used by the framework to cause the control to retrieve its values from the form
UpdateFormValues(context.Context)
Validate(ctx context.Context) bool
ValidationState() ValidationState
ValidationType(*Event) ValidationType
SetValidationType(typ ValidationType) ControlI
ChildValidationChanged()
// SaveState tells the control whether to save the basic state of the control, so that when the form is reentered, the
// data in the control will remain the same. This is particularly useful if the control is used as a filter for the
// contents of another control.
SaveState(context.Context, bool)
MarshalState(m maps.Setter)
UnmarshalState(m maps.Loader)
// Shortcuts for translation
// GT translates strings using the Goradd dictionary.
GT(format string) string
// T translates strings using the application provided dictionary.
T(message string, params ...interface{}) string
TPrintf(format string, params ...interface{}) string
// Serialization helpers
Restore()
Cleanup()
// API
SetIsRequired(r bool) ControlI
Serialize(e Encoder) (err error)
Deserialize(d Decoder) (err error)
ApplyOptions(ctx context.Context, o ControlOptions)
AddControls(ctx context.Context, creators ...Creator)
DataConnector() DataConnector
SetDataConnector(d DataConnector) ControlI
RefreshData(data interface{})
UpdateData(data interface{})
WatchDbTables(ctx context.Context, nodes... query.NodeI)
WatchDbRecord(ctx context.Context, n query.NodeI, pk string)
WatchChannel(ctx context.Context, channel string)
}
type attributeScriptEntry struct {
id string // id of the object to execute the command on. This should be the id of the control, or a a related html object.
f string // the jquery function to call
commands []interface{} // parameters to the jquery function
}
// ControlBase is the basis for UI controls and widgets in goradd. It corresponds to a standard html form object or tag, or a custom javascript
// widget. A Control renders a tag and everything inside of the tag, but can also include a wrapper which associates
// a label, instructions and error messages with the tag. A Control can also associate javascript
// with itself to make sure the javascript is loaded on the page when the control is drawn, and can render
// javascript that will initialize a custom javascript widget.
//
// A Control can have child Controls. It
// can either allow the framework to automatically draw the child Controls as part of the inner-html of
// the ControlBase, can use a template to draw the Child controls, or manually draw them. The ControlBase is part
// of a hierarchical tree structure, with the Form being the root of the tree.
//
// A Control is part of a system that will reflect the state of the control between the client and server.
// When a user updates a control in the browser and performs an action that requires a response from the
// server, the goradd javascript will gather up all the changes in the form and send those to the server.
// The control can read those values and update its own internal state, so that from the perspective
// of the programmer referring to the control, the values in the ControlBase are the same as what the user sees in a browser.
//
// This ControlBase struct is a mixin that all controls should use. You would not normally create a ControlBase directly,
// but rather create one of the "subclasses" of ControlBase. See the control package for Controls that implement
// standard html widgets.
type ControlBase struct {
base.Base
// id is the id passed to the control when it is created, or assigned automatically if empty.
id string
// page is a pointer to the page that encloses the entire control tree.
page *Page
// parentId is the id of the immediate parent control of this control. Only the form object will not have a parent.
// We use the id here to prevent a memory leak if we remove the control from the form.
parentId string
// children are the child controls that belong to this control. They are cached for speed, and to allow
// children of controls to be accessed even when the control is not part of the form.
children []ControlI // Child controls
// Tag is text of the tag that will enclose the control, like "div" or "input"
Tag string
// IsVoidTag should be true if the tag should not have a closing tag, like "img"
IsVoidTag bool
// hasNoSpace is for special situations where we want no space between this and the next tag. Spans in particular may need this.
hasNoSpace bool
// attributes are the collection of custom attributes to apply to the control. This does not include all the
// attributes that will be drawn, as some are added temporarily just before drawing by GetDrawingAttributes()
attributes html.Attributes
// text is a multi purpose string that can be button text, inner text inside of tags, etc. depending on the control.
text string
// textLabelMode describes how to draw the internal label
textLabelMode html.LabelDrawingMode
// textIsHtml will prevent the text output from being escaped
textIsHtml bool
// attributeScripts are commands to send to our javascript to redraw portions of the control via ajax.
attributeScripts []attributeScriptEntry
// isRequired indicates that we will require a value during validation
isRequired bool
// isHidden indicates that we will not draw the control, but rather an invisible placeholder for the control.
isHidden bool
// isOnPage indicates we have drawn the control at some point in the past
isOnPage bool
// shouldAutoRender indicates that we will eventually draw the control even if it is not drawn directly.
shouldAutoRender bool
// internal status functions. Do not serialize.
// isModified will cause the control to redraw as part of the response.
isModified bool
// isRendering is true when we are in the middle of rendering the control.
isRendering bool
// wasRendered indicates that the page was drawn during the current response.
wasRendered bool
// isBlock is true to use a div for the wrapper, false for a span
isBlock bool
// ErrorForRequired is the error that will display if a control value is required but not set.
ErrorForRequired string
// ValidMessage is the message to display if the control has successfully been validated.
// Leave blank if you don't want a message to show when valid.
// Can be useful to contrast between invalid and valid controls in a busy form.
ValidMessage string
// validationMessage is the current validation message that will display when drawing the control
// This gets copied from ValidMessage at drawing time if the control is in an invalid state
validationMessage string
// validationState is the current validation state of the control, and will effect how the control is drawn.
validationState ValidationState
// validationType indicates how the control will validate itself. See ValidationType for a description.
validationType ValidationType
// validationTargets is the list of control IDs to target validation
validationTargets []string
// This blocks a parent from validating this control. Useful for dialogs, and other situations where sub-controls should control their own space.
blockParentValidation bool
// actionValue is the value that will be provided as the ControlValue for any actions that are triggered by this control.
actionValue interface{}
// events are all the events added by the control user that the control might trigger
events EventMap
// eventCounter is used to generate a unique id for an event to help us route the event through the system.
eventCounter EventID
// shouldSaveState indicates that we should save parts of our state into a session variable so that if
// the client should come back to the form, we will attempt to restore the state of the control. The state
// in this situation would be the user's input, so text in a textbox, or the selection from a list.
shouldSaveState bool
// encoded is used during the serialization process to prevent encoding a control multiple times.
encoded bool
dataConnector DataConnector
watchedKeys map[string]string
// anything added here needs to be also added to the GOB encoder!
}
// Init is used by ControlBase implementations to initialize the standard control structure. You would only call this if you
// are subclassing one of the standard controls.
// ControlBase implementations should call this immediately after a control is created.
// The ControlBase subclasses should have their own Init function that
// call this superclass function. This Init function sets up a parent-child relationship with the given parent
// control, and sets up data structures to use the control in object-oriented ways with virtual functions.
// The id is the control id that will appear as the id in html. Leave blank for the system to create a unique id for you.
func (c *ControlBase) Init(parent ControlI, id string) {
c.attributes = html.NewAttributes()
if parent != nil {
c.page = parent.Page()
c.id = c.page.GenerateControlID(id)
}
c.this().SetParent(parent)
c.isModified = true
}
// this supports object oriented features by giving easy access to the virtual function interface.
// Subclasses should provide a duplicate. Calls that implement chaining should return the result of this function.
func (c *ControlBase) this() ControlI {
return c.Self.(ControlI)
}
// Restore is called after the control has been deserialized. It notifies the control tree so that it
// can restore internal pointers.
// TODO: Serialization is not yet implemented
func (c *ControlBase) Restore() {
}
// ID returns the id assigned to the control. If you do not provide an ID when the control is created,
// the framework will give the control a unique id.
func (c *ControlBase) ID() string {
return c.id
}
// Extract the control from an interface. This is for package private use, when called through the interface.
func (c *ControlBase) control() *ControlBase {
return c
}
// PreRender is called by the framework to notify the control that it is about to be drawn. If you
// override it, be sure to also call this parent function as well.
func (c *ControlBase) PreRender(ctx context.Context, buf *bytes.Buffer) error {
form := c.ParentForm()
if c.Page() == nil ||
form == nil ||
c.Page() != form.Page() {
return NewError(ctx, fmt.Sprintf("Control %s can not be drawn because it is not a member of a form that is on the override.", c.ID()))
}
if c.wasRendered || c.isRendering {
return NewError(ctx, fmt.Sprintf("Control %s has already been drawn.", c.ID()))
}
// Because we may be rerendering a parent control, we need to make sure all "child" controls are marked as NOT being on the form
// before rendering it again.
for _, child := range c.children {
child.control().markOnPage(false)
}
// Finally, let's specify that we have begun rendering this control
c.isRendering = true
return nil
}
// Draw renders the control structure into the given buffer.
func (c *ControlBase) Draw(ctx context.Context, buf *bytes.Buffer) (err error) {
if err = c.this().PreRender(ctx, buf); err != nil {
return err
}
var h string
if c.isHidden {
// We are invisible, but not using a wrapper. This creates a problem, in that when we go visible, we do not know what to replace
// To fix this, we create an empty, invisible control in the place where we would normally draw
h = "<span id=\"" + c.this().ID() + "\" style=\"display:none;\" data-grctl></span>\n"
} else {
h = c.this().DrawTag(ctx)
}
if !config.Minify && GetContext(ctx).RequestMode() != Ajax {
s := html.Comment(fmt.Sprintf("ControlBase Type:%s, Id:%s", c.Type(), c.ID())) + "\n"
buf.WriteString(s)
}
buf.WriteString(h)
response := c.ParentForm().Response()
c.this().PutCustomScript(ctx, response)
c.GetActionScripts(response)
c.this().PostRender(ctx, buf)
return
}
// PutCustomScript is called by the framework to ask the control to inject any javascript it needs into the form.
// In particular, this is the place where Controls add javascript that transforms the html into a custom javascript control.
// A ControlBase implementation does this by calling functions on the response object.
// This implementation is a stub.
func (c *ControlBase) PutCustomScript(ctx context.Context, response *Response) {
}
// NeedsRefresh returns true if the control needs to be completely redrawn. Generally you control
// this by calling Refresh(), but subclasses can implement other ways of detecting this.
func (c *ControlBase) NeedsRefresh() bool {
return c.isModified
}
// DrawAjax will be called by the framework during an Ajax rendering of the ControlBase. Every ControlBase gets called. Each ControlBase
// is responsible for rendering itself. Some objects automatically render their child objects, and some don't,
// so we detect whether the parent is being rendered, and assume the parent is taking care of rendering for
// us if so.
//
// Override if you want more control over ajax drawing, like if you detect parts of your control that have changed
// and then want to draw only those parts. This will get called on every control on every ajax draw request.
// It is up to you to test the blnRendered flag of the control to know whether the control was already rendered
// by a parent control before drawing here.
func (c *ControlBase) DrawAjax(ctx context.Context, response *Response) (err error) {
if c.this().NeedsRefresh() {
// simply re-render the control and assume rendering will handle rendering its children
func() {
// wrap in a function to get deferred PutBuffer to execute immediately after drawing
buf := buf2.GetBuffer()
defer buf2.PutBuffer(buf)
err = c.this().Draw(ctx, buf)
response.SetControlHtml(c.ID(), buf.String())
}()
} else {
// add attribute changes
if c.attributeScripts != nil {
for _, entry := range c.attributeScripts {
response.ExecuteControlCommand(entry.id, entry.f, entry.commands...)
}
c.attributeScripts = nil
}
// ask the child controls to potentially render, since this control doesn't need to
for _, child := range c.children {
if child.IsOnPage() || child.ShouldAutoRender() {
err = child.DrawAjax(ctx, response)
}
if err != nil {
return
}
}
}
return
}
// PostRender is called by the framework at the end of drawing, and is the place where controls
// do any post-drawing cleanup needed.
func (c *ControlBase) PostRender(ctx context.Context, buf *bytes.Buffer) (err error) {
// Update watcher
//if ($This->objWatcher) {
//$This->objWatcher->makeCurrent();
//}
c.isRendering = false
c.wasRendered = true
c.isOnPage = true
c.isModified = false
c.attributeScripts = nil // Entire control was redrawn, so don't need these
return
}
// DrawTag is responsible for drawing the ControlBase's tag itself.
// ControlBase implementations can override this to draw the tag in a different way, or draw more than one tag if
// drawing a compound control.
func (c *ControlBase) DrawTag(ctx context.Context) string {
// TODO: Implement this with a buffer to reduce string allocations
var ctrl string
log.FrameworkDebug("Drawing tag: " + c.ID())
attributes := c.this().DrawingAttributes(ctx)
if c.IsVoidTag {
ctrl = html.RenderVoidTag(c.Tag, attributes)
} else {
buf := buf2.GetBuffer()
defer buf2.PutBuffer(buf)
if err := c.this().DrawInnerHtml(ctx, buf); err != nil {
panic(err)
}
if err := c.RenderAutoControls(ctx, buf); err != nil {
panic(err)
}
if c.Tag == "" {
ctrl = buf.String() // a wrapper with no tag. Just inserts functionality and draws its children.
} else if c.hasNoSpace {
ctrl = html.RenderTagNoSpace(c.Tag, attributes, buf.String())
} else {
ctrl = html.RenderTag(c.Tag, attributes, buf.String())
}
}
return ctrl
}
// RenderAutoControls is an internal function to draw controls marked to autoRender. These are generally used for hidden controls
// that can be shown without impacting layout, or that are scripts only. ControlBase implementations that need to
// put these controls in particular locations on the form can override this.
func (c *ControlBase) RenderAutoControls(ctx context.Context, buf *bytes.Buffer) (err error) {
// Figuring out where to draw these controls can be difficult.
for _, ctrl := range c.children {
if ctrl.ShouldAutoRender() &&
!ctrl.WasRendered() {
err = ctrl.Draw(ctx, buf)
if err != nil {
break
}
}
}
return
}
// DrawTemplate is used by the framework to draw the ControlBase with a template.
// Controls that use templates should use this function signature for the template. That will override this one, and
// we will then detect that the template was drawn. Otherwise, we detect that no template was defined and it will move
// on to drawing the controls without a template, or just the text if text is defined.
func (c *ControlBase) DrawTemplate(ctx context.Context, buf *bytes.Buffer) (err error) {
// Don't change this to use some kind of function injection, as such things are not serializable
return NewFrameworkError(FrameworkErrNoTemplate)
}
// DrawInnerHtml is used by the framework to draw just the inner html of the control, if the control is not a self
// terminating (void) control. Sub-controls can override this.
func (c *ControlBase) DrawInnerHtml(ctx context.Context, buf *bytes.Buffer) (err error) {
if err = c.this().DrawTemplate(ctx, buf); err == nil {
return
} else if appErr, ok := err.(FrameworkError); !ok || appErr.Err != FrameworkErrNoTemplate {
return
}
err = nil
if c.children != nil && len(c.children) > 0 {
err = c.this().DrawChildren(ctx, buf)
return
}
c.this().DrawText(ctx, buf)
return
}
// DrawChildren renders the child controls that have not yet been drawn into the buffer.
func (c *ControlBase) DrawChildren(ctx context.Context, buf *bytes.Buffer) (err error) {
for _, child := range c.children {
if !child.WasRendered() {
err = child.Draw(ctx, buf)
if err != nil {
break
}
}
}
return
}
// DrawText renders the text of the control, escaping if needed.
func (c *ControlBase) DrawText(ctx context.Context, buf *bytes.Buffer) {
if c.text != "" {
text := c.text
if !c.textIsHtml {
text = gohtml.EscapeString(text)
}
buf.WriteString(text)
}
}
// SetAttribute sets an html attribute of the control. You can manually set most any attribute, but be careful
// not to set the id attribute, or any attribute that is managed by the control itself. If you are setting
// a data-* attribute, use SetDataAttribute instead. If you are adding a class to the control, use AddAttributeValue.
func (c *ControlBase) SetAttribute(name string, val interface{}) ControlI {
if name == "id" {
panic("You can only set the 'id' attribute of a control when it is created")
}
changed, err := c.attributes.SetChanged(name, html.AttributeString(val))
if err != nil {
panic(err)
}
if changed {
// The val passed in might be a calculation, so we need to get the ultimate new value
v2 := c.attributes.Get(name)
// We are recording here that the attribute intends to change. If we are responding to an ajax
// request, we will send back a command to only change the attribute on the control if the
// control does not get completely redrawn. If the control is completely redrawn, the new
// attribute will automatically be drawn, so there would be no need to also send an attribute change command.
c.AddRenderScript("attr", name, v2)
}
return c.this()
}
// Return the value of a custom attribute. Note that this will not return values that are set only during
// drawing and that are managed by the ControlBase implementation.
func (c *ControlBase) Attribute(name string) string {
return c.attributes.Get(name)
}
// HasAttribute returns true if the control has the indicated custom attribute defined.
func (c *ControlBase) HasAttribute(name string) bool {
return c.attributes.Has(name)
}
// DrawingAttributes is called by the framework just before drawing a control, and should
// return a set of attributes that should override those set by the user. This allows controls to set attributes
// that should take precedence over other attributes, and that are critical to drawing the
// tag of the control. This function is designed to only be called by ControlBase implementations.
func (c *ControlBase) DrawingAttributes(ctx context.Context) html.Attributes {
a := html.NewAttributesFrom(c.attributes)
a.SetID(c.id) // make sure the control id is set at a minimum
a.SetDataAttribute("grctl", "") // make sure control is registered. Overriding controls can put a control name here.
if c.isRequired {
a.Set("aria-required", "true")
}
channels := stringmap.JoinStrings(c.watchedKeys, "=", ";")
if channels != "" {
a.SetDataAttribute("grWatch", channels)
}
return a
}
// SetDataAttribute will set a data-* attribute. You do not need to include the "data-" in the name, it will be added
// automatically.
func (c *ControlBase) SetDataAttribute(name string, val interface{}) {
var v string
var ok bool
if v, ok = val.(string); !ok {
v = fmt.Sprint(v)
}
changed, err := c.attributes.SetDataAttributeChanged(name, v)
if err != nil {
panic(err)
}
if changed {
c.AddRenderScript("data", name, v) // Use the data method to set the data during ajax requests
}
}
func (c *ControlBase) MergeAttributes(a html.Attributes) ControlI {
c.attributes.Merge(a)
return c.this()
}
// ProcessAttributeString is used by the drawing template to let you set attributes in the draw tag.
// Attributes are of the form `name="value"`.
func (c *ControlBase) ProcessAttributeString(s string) ControlI {
if s != "" {
c.attributes.Merge(s)
}
return c.this()
}
// AddAttributeValue will add a class or classes to the control. If adding multiple classes at once, separate them with
// a space.
func (c *ControlBase) AddClass(class string) ControlI {
if changed := c.attributes.AddClassChanged(class); changed {
// Note here. We cannot just draw the class, because DrawingAttributes might return
// a class, and DrawingAttributes requires a context. So we coordinate with goradd.js
// to be able to add and remove a class.
c.AddRenderScript("class", "+" + class)
}
return c.this()
}
// RemoveClass will remove the named class from the control.
func (c *ControlBase) RemoveClass(class string) ControlI {
if changed := c.attributes.RemoveClass(class); changed {
c.AddRenderScript("class", "-" + class)
}
return c.this()
}
// HasClass returns true if the class has been assigned to the control from the GO side. We do not currently detect
// class changes done in javascript.
func (c *ControlBase)HasClass(class string) bool {
return c.attributes.HasClass(class)
}
// Attributes returns a pointer to the attributes of the control. Use this with caution.
// Some controls setup attributes at initialization time, so you could potentially write over those.
// Also, if you change attributes during an ajax call, the changes will not be reflected unless you redraw
// the control. The primary use for this function is to allow controls to set up attributes during initialization.
func (c *ControlBase) Attributes() html.Attributes {
return c.attributes
}
// AddRenderScript adds a jQuery command to be executed on the next ajax draw.
// These commands allow javascript to change an aspect of the control without
// having to redraw the entire control. This should be used by ControlBase implementations only.
func (c *ControlBase) AddRenderScript(f string, params ...interface{}) {
c.attributeScripts = append(c.attributeScripts, attributeScriptEntry{id: c.ID(), f: f, commands: params})
}
// AddRelatedRenderScript adds a render script for a related html object. This is primarily used by control implementations.
func (c *ControlBase) AddRelatedRenderScript(id string, f string, params ...interface{}) {
c.attributeScripts = append(c.attributeScripts, attributeScriptEntry{id: id, f: f, commands: params})
}
// Parent returns the parent control of the control. All controls have a parent, except the Form control.
func (c *ControlBase) Parent() ControlI {
if c.Page().HasControl(c.parentId) {
return c.Page().GetControl(c.parentId)
}
return nil
}
// Children returns the child controls of the control.
func (c *ControlBase) Children() []ControlI {
return c.children
}
// RangeAllChildren recursively calls the given function on every child control and subcontrol.
// It calls the function on the child controls of each control first, and then on the control itself.
func (c *ControlBase) RangeAllChildren(f func(child ControlI)) {
for _, child := range c.children {
child.RangeAllChildren(f)
f(child)
}
}
// RangeSelfAndAllChildren recursively calls the given function on this control and every child control and subcontrol.
// It calls the function on the child controls of each control first, and then on the control itself.
func (c *ControlBase) RangeSelfAndAllChildren(f func(ctrl ControlI)) {
c.RangeAllChildren(f)
f(c.this())
}
// Remove removes the current control from its parent. After this is done, the control and all its child items will
// not be part of the drawn form, but the child items will still be accessible through the control itself.
func (c *ControlBase) Remove() {
if c.parentId != "" {
c.Parent().control().removeChild(c.this().ID(), true)
if !c.shouldAutoRender {
//c.Refresh() // TODO: Do this through ajax
}
} else {
c.page.removeControl(c.this().ID())
}
}
// RemoveChild removes the given child control from both the control and the form.
func (c *ControlBase) RemoveChild(id string) {
c.removeChild(id, true)
}
// removeChild is a private function that will remove a child control from the current control
func (c *ControlBase) removeChild(id string, fromPage bool) {
for i, v := range c.children {
if v.ID() == id {
c.children = append(c.children[:i], c.children[i+1:]...) // remove found item from list
if fromPage {
v.control().removeChildrenFromPage()
c.page.removeControl(id)
}
v.control().parentId = ""
break
}
}
}
func (c *ControlBase) removeChildrenFromPage() {
c.RangeAllChildren(func(child ControlI) {
c.page.removeControl(child.ID())
})
}
// RemoveChildren removes all the child controls from this control and the form so that the memory manager can delete them.
func (c *ControlBase) RemoveChildren() {
for _, child := range c.children {
child.control().removeChildrenFromPage()
c.page.removeControl(child.ID())
child.control().parentId = ""
}
c.children = nil
}
// SetParent sets the parent of the control. Use this primarily if you are responding to some kind of user
// interface that will move a child ControlBase from one parent ControlBase to another.
func (c *ControlBase) SetParent(newParent ControlI) {
if c.parentId == "" {
c.control().addChildControlsToPage()
} else {
c.Parent().control().removeChild(c.ID(), newParent == nil)
if !c.shouldAutoRender {
//c.parent.Refresh()
}
}
if newParent != nil {
c.parentId = newParent.ID()
c.Parent().control().addChildControl(c.this())
if !c.shouldAutoRender {
// TODO: insert into DOM instead of c.parent.Refresh()
}
} else {
c.parentId = ""
}
c.page.addControl(c.this())
if c.shouldAutoRender && newParent != nil {
//c.Refresh()
}
// TODO: Refresh as needed, but without refreshing the form
}
// Child returns the child control with the given id.
// TODO: This should be a map, both to speed it up, and add the ability to sort it
func (c *ControlBase) Child(id string) ControlI {
for _, c := range c.children {
if c.ID() == id {
return c
}
}
return nil
}
func (c *ControlBase) addChildControlsToPage() {
for _, child := range c.children {
child.control().addChildControlsToPage()
c.page.addControl(child)
}
}
// Private function called by setParent on parent function
func (c *ControlBase) addChildControl(child ControlI) {
if c.children == nil {
c.children = make([]ControlI, 0)
}
c.children = append(c.children, child)
}
// ParentForm returns the form object that encloses this control.
func (c *ControlBase) ParentForm() FormI {
return c.page.Form()
}
// Page returns the page object associated with the control.
func (c *ControlBase) Page() *Page {
return c.page
}
// Refresh will force the control to be completely redrawn on the next update.
func (c *ControlBase) Refresh() {
c.isModified = true
}
// SetIsRequired will set whether the control requires a value from the user. Setting it to true
// will cause the ControlBase to check this during validation, and show an appropriate error message if the user
// did not enter a value.
func (c *ControlBase) SetIsRequired(r bool) ControlI {
c.isRequired = r
return c.this()
}
// IsRequired returns true if the control requires input from the user to pass validation.
func (c *ControlBase) IsRequired() bool {
return c.isRequired
}
// ValidationMessage is the currently set validation message that will print with the control. Normally this only
// gets set when a validation error occurs.
func (c *ControlBase) ValidationMessage() string {
return c.validationMessage
}
// SetValidationError sets the validation error to the given string. It will also handle setting the wrapper class
// to indicate an error. Override if you have a different way of handling errors.
func (c *ControlBase) SetValidationError(e string) {
if c.validationMessage != e {
c.validationMessage = e
if e == "" {
c.validationState = ValidationWaiting
c.AddRenderScript("removeAttr", "aria-invalid")
} else {
c.validationState = ValidationInvalid
c.AddRenderScript("attr", "aria-invalid", "true")
}
if c.Parent() != nil {
c.Parent().ChildValidationChanged() // notify parent wrappers
}
}
}
func (f *ControlBase) ResetValidation() {
f.RangeSelfAndAllChildren(func(ctrl ControlI) {
c := ctrl.control()
var changed bool
if c.validationMessage != "" {
c.validationMessage = ""
changed = true
}
if c.validationState != ValidationWaiting {
c.validationState = ValidationWaiting
changed = true
}
if changed {
if p := c.Parent(); p != nil {
p.ChildValidationChanged()
}
}
})
}
// ChildValidationChanged is sent by the framework when a child control's validation message
// has changed. Parent controls can use this to change messages or attributes in response.
func (c *ControlBase) ChildValidationChanged() {
if c.Parent() != nil {
c.Parent().ChildValidationChanged()
}
}
// ValidationState returns the current ValidationState value.
func (c *ControlBase) ValidationState() ValidationState {
return c.validationState
}
// SetText sets the text of the control. Not all controls use this value.
func (c *ControlBase) SetText(t string) ControlI {
if t != c.text {
c.text = t