//Add() is the method that is called in response to an element being //added to the collection (self.todos). This is the "magic" turns an //instance of todo into a view. func (self *todoApp) Add(length int, newObj s5.Model) { model := newObj.(*todo) //note: There are two legal things that can be passed to any //note: of the tag creation methods. Sadly, there is no way //note: to typecheck these until runtime (it is checked then). //note: //note: The two legal things are some type of option, such as //note: Class() or ModelId() that affect the resulting tag. //note: One of the common types of "options" is something creates //note: a constraint between a dom "piece" of the tag being //note: being constructed and some value, usually in the model. //note: //note: The other leagl item is another tag, that will be added //note: as a child of the one it is neted in. This lack of type //note: safety has been chosen for convenience of notation. tree := s5.LI( //LI: Pass in a model ID to generate unique id for this tag, //LI: and make easy to remove the whole subtree by id. s5.ModelId(model), //LI: constraint that toggles the completed property s5.CssExistence(completed, model.done), //LI: constraint that toggles the editing property s5.CssExistence(editing, model.editing), s5.DIV( //DIV: just one CSS class to make it look nice s5.Class(view), s5.INPUT( //INPUT: has a CSS class "toggle" to make it look nice s5.Class(toggle), //INPUT: we force the "type" of this to be the constant "checkbox" (possibly overkill) s5.HtmlAttrEqual(s5.TYPE, s5.NewStringSimple("checkbox")), //INPUT: make the checked attr be equal to the model's done s5.PropEqual(s5.CHECKED, model.done), //INPUT: when clicked, it toggles the value on the model s5.Event(s5.CHANGE, func(e jquery.Event) { model.done.Set(!model.done.Value()) }), ), s5.LABEL( //LABEL: We use a constraint to bind the name attribute of the //LABEL: model to the label's displayed text. s5.TextEqual(model.name), //LABEL: Double clicking on the label causes edit mode s5.Event(s5.DBLCLICK, func(jquery.Event) { model.editing.Set(true) //XXX UGH, don't have a handle to the input object in := s5.HtmlIdFromModel("INPUT", model).Dom() in.SetVal(model.name.Value()) in.Focus() in.Select() }), ), s5.BUTTON( //BUTTON: destroy class makes it look nice s5.Class(destroy), //BUTTON: click function that calls remove on the list //BUTTON: element that was used to create this whole structure //JQUERY: Neither of the jquery params are used. s5.Event(s5.CLICK, func(jquery.Event) { //note: we are calling remove on the *collection* which //note: will end up calling the Remove() method of our //note: joiner. If we don't tell the collection that the //note: model was removed, we could end up with a display //note: that doesn't correctly reflect the constraints //note: state (since these constraints would have dependencies //note: one items no longer visible). self.todos.Remove(model) }), ), //BUTTON ), //DIV s5.INPUT( //INPUT: Use a model to make this input easy to find s5.ModelId(model), //INPUT: edit CSS class to make it look nice s5.Class(edit), //INPUT: wire the placeholder to be name of the model... (overkill?) s5.HtmlAttrEqual(s5.PLACEHOLDER, model.name), //INPUT:the spec calls for escape to cancel editing with no change //INPUT:and for return to commit the changes EXCEPT if the //INPUT:user edited out all the text, then we should delete the //INPUT:whole thing //JQUERY: This uses the jquery selector to get the value of the input. //JQUERY: This uses the event object to get the keyboard code. s5.Event(s5.KEYDOWN, func(e jquery.Event) { //note: This type of "event handler" is the glue that //note: connects a user action to something that manipulates //note: the model. Most event handlers do not need to //note: manipulate the view as well because they have constraints //note: that connect the model to the view. switch e.Which { case 13: //This conversion is a no-op other than the go language type. //The e.Target value is ALREADY a jquery object, but it's not //typed correctly for this function to use conveniently. j := s5.WrapJQuery(jquery.NewJQuery(e.Target)) v := strings.Trim(j.Val(), CUTSET) //check for the special case of making a name=="" if v == "" { self.todos.Remove(model) } else { //just reset the model name and it propagates to display model.name.Set(v) } j.Blur() fallthrough case 27: model.editing.Set(false) primaryInput.Dom().Focus() } }), ), //INPUT ).Build() listContainer.Dom().Append(tree) }
//Start is called by concorde once the DOM is fully loaded. Most of the //application intialization code should be put in here. func (self *todoApp) Start() { //Setup an event handler for the primary input field. The called func //creates model instance and puts in the list of todos. // JQUERY: Any use of jquery is suspect as it allows many non type-safe operations. primaryInput.Dom().On(s5.CHANGE, func(event jquery.Event) { if !self.createTodo(primaryInput.Dom().Val()) { event.PreventDefault() } }) //We need to attach the self.numNotDone string to the proper place //in the DOM. Note that we use the lower-level jquery selector //(Select().Children())) plus NewTextAttr() we needed the string //child of span#todo-count (so we can't use HtmlId) and because //that object is directly in the HTML file. // JQUERY: Any use of jquery is suspect as it allows many non type-safe operations. selector := jquery.NewJQuery(todoCount.TagName() + "#" + todoCount.Id()) todoCountSelect := selector.Children("strong") s5.Equality(s5.NewTextAttr(s5.WrapJQuery(todoCountSelect)), self.numNotDone) //We need to attach the self.plural string to the proper place //in the dom. s5.Equality(pluralSpan.TextAttribute(), self.plural) //These two calls attach the inverse of the empty attribute derived //from the model collection the display property (turning them on //when the list is not empty). Note that we can't use the simpler //"Equality()" because we want to invert the value. The BooleanInverter //is a built in constraint function. sectionMain.DisplayAttribute().Attach( s5.NewBooleanInverter(self.todos.EmptyAttribute())) footer.DisplayAttribute().Attach( s5.NewBooleanInverter(self.todos.EmptyAttribute())) //This connects the display property of the clearCompleted to the boolean //that is true if some of the elements are done. This effectively //turns on the button when there are some elements in the list and //some of those elements are done. s5.Equality(clearCompleted.DisplayAttribute(), self.someDone) //This connects the display in the button to the number of done //elements. Note that this wont be visible if there are no //done elements. s5.Equality(numCompleted.TextAttribute(), self.numDone) //This is the event handler for click on the clearCompleted //dom element. We just walk the list of objects building a kill list, //then we destroy all the items in the kill list //JQUERY: Neither of the jquery params are used. clearCompleted.Dom().On(s5.CLICK, func(jquery.Event) { all := self.todos.All() if len(all) == 0 { return } dead := make([]s5.Model, len(all)) ct := 0 for _, model := range all { if model.(*todo).done.Value() { dead[ct] = model ct++ } } for _, d := range dead { self.todos.Remove(d) } }) //toggleAll's behavior is to toggle any items that are not already //marked done, unless they are all marked done in which they should all //be umarked //JQUERY: Neither of the jquery params are used. toggleAll.Dom().On(s5.CLICK, func(jquery.Event) { desired := true //Compare the output of the constraints to see if all are done if self.todos.LengthAttribute().Value() == self.numDone.Value() { desired = false } for _, m := range self.todos.All() { m.(*todo).done.Set(desired) } }) //These are discussed below. These are constraints that depend //on *all* the values in the list. self.dependsOnAll() }