can.view.tag and can.view.attr #636

Closed
justinbmeyer opened this Issue Dec 31, 2013 · 7 comments

Comments

Projects
None yet
5 participants
@justinbmeyer
Contributor

justinbmeyer commented Dec 31, 2013

I want to clean up, document, and expose the hookup logic of can.view that can-value and can.Component use. Here's my proposed docs for can.view.tag (can.view.attribute will come later):

can.view.tag

@function can.view.tag

Register custom behavior for a given tag.

@Signature can.view.tag(tagName, tagHandler(el, tagData) )

Registers the tagHandler callback when tagName is found
in a template.

@param {String} tagName A lower-case, hypenated or colon-seperated html
tag. Example: "my-widget" or "my:widget". It is considered a best-practice to
have a hypen or colon in all custom-tag names.

@param {function(HTMLElement,can.view.tagData):can.view.Scope} tagHandler(el, tagData)

Adds custom behavior to el. If tagHandler returns data, it is used to
render tagData.subtemplate and the result is inserted as the childNodes of el.

Use

can.view.tag is a low-level way to add custom behavior to custom elements. Often, you
typically want to do this with [can.Component]. However, can.view.tag is
useful for when [can.Component] might be considered overkill. For example, the
following creates a jQueryUI everytime a
<jqui-datepicker> element is found:

can.view.tag("jqui-datepicker", function(el, tagData){
  $(el).datepicker()
})

The tagHandler's [can.view.tagData tagData] argument is an object
that contains the mustache [can.view.Scope scope] and helper [can.view.Options options]
where el is found and a [can.view.renderer subtemplate] that renders the contents of the
template within the custom tag.

Getting values from the template

tagData.scope can be used to read data from the template. For example, if I wanted
the value of "format" within the current template, it could be read like:

can.view.tag("jqui-datepicker", function(el, tagData){
  $(el).datepicker({format: tagData.scope.attr("format")})
})

var template = can.mustache("<jqui-datepicker></jqui-datepicker>")
template({format: "mm/dd/yy"})

tagData.options contains the helpers and partials provided
to the template. A helper function might need to be called to get the current value of format like:

can.view.tag("jqui-datepicker", function(el, tagData){
  $(el).datepicker({format: tagData.options.attr("helpers.format")()})
})

var template = can.mustache("<jqui-datepicker></jqui-datepicker>")
template({},{format: function(){
  return "mm/dd/yy"
}})

Responding to changing data

Often, data passed to a template is observable. If you use can.view.tag, you must
listen and respond to chagnes yourself. Consider if format is a compute like:

var dateFormat = can.compute("mm/dd/yy")

You would want to update the datepicker if format changes like:

can.view.tag("jqui-datepicker", function(el, tagData){
  var formatCompute = tagData.scope.attr("format");

  // we will need to unbind this, so keep a reference
  var changeHandler = function(ev, newVal){
    $(el).datepicker("option","format", newVal});
  }

  formatCompute.bind("change",changeHandler)

  $(el)
    // When this element is removed from the page
    // cleanup the change binding.
    .bind("removed", function(){
      formatCompute.unbind("change",changeHandler)
    })
    .datepicker({format: formatCompute});

})

var template = can.mustache("<jqui-datepicker/>")
template({format: dateFormat})

Subtemplate

If content is found within a custom tag like:

var template = can.mustache(
  "<my-form>\
     <input value="{{first}}"/>\
     <input value="{{last}}"/>\
   </my-form>")

A seperate template function is compiled and passed
as tagData.subtemplate. That subtemplate can
be rendered with custom data and options. For example:

can.view.tag("my-form", function(el, tagData){
   var frag = tagData.subtemplate({
     first: "Justin"
   }, tagData.options)

   $(el).html( frag )
})

template({
  last: "Meyer" 
})

In this case, the sub-template will not get a value for last. To
include the original data in the subtemplate's scope, [can.view.Scope::add add] to
the old scope like:

can.view.tag("my-form", function(el, tagData){
   var frag = tagData.subtemplate(
     tagData.scope.add({ first: "Justin" }), 
     tagData.options)

   $(el).html( frag )
})

template({
  last: "Meyer" 
})

can.view.attr

@function can.view.attr

Register custom behavior for an attribute.

@Signature can.view.attr(attrName, attrHandler(el, attrData) )

Registers the attrHandler callback when attrName is found
in a template.

@param {String|RegExp} attrName A lower-case attribute name or regular expression
that matches attribute names. Examples: "my-fill" or /my-\w/.

@param {function(HTMLElement,can.view.attrData)} attrHandler(el, attrData)

A function that adds custom behavior to el.

Use

can.view.attr is used to add custom behavior to elements that contain a
specified html attribute. Typically it is used to mixin behavior (whereas
[can.view.tag] is used to define behavior).

The following example adds a jQueryUI tooltip to any element that has
a tooltip attribute like <div tooltip="Click to edit">Name</div>.

can.view.attr("tooltip", function( el, attrData ) {
  $(el).tooltip({content: el.getAttribute("tooltip")})
})

Listening to attribute changes

In the previous example, the content of the tooltip was static. However,
it's likely that the tooltip's value might change. For instance, the template
the tooltip is found in might look like:

<div tooltip="{{tooltipContent}}">Name</div>

The [can.events.attributes attributes] event can be used to listen to when
the toolip attribute changes its value like:

can.view.attr("tooltip", function( el, attrData ) {
  var $el = $(el);

  var updateContent = function(){
    var content = $el.attr("tooltip");
    if(content) {
      $(el).tooltip({content: el.getData("tooltip")})
    } else {
      $(el).tooltip("destroy")
    } 
  }
  $el.bind("attributes", function(ev){
    if( ev.attributeName == "tooltip" ) {
      updateContent();
    }
  })

  updateContent();

})

Reading values from the scope.

It's common that attribute mixins need complex, observable data to
perform rich behavior. The attribute mixin is able to read
data in its element's [can.view.Scope scope]. The values
to read are often the attribute values.

In the following example, the tooltip uses the attribute value
to lookup the tooltip options. The template the tooltip is
rendered in might looke like:

<div tooltip="tooltipOptions">Name</div>

And be rendered like:

var options = new can.Map({
  content: "Edit content",
  position: { my: "left+15 center", at: "right center" }
})

template({
  tooltipOptions: options
})

When binding on observable data, make sure to unbind from the observable if
the element is removed from the page or the attribute changed or removed.

can.view.attr("tooltip", function( el, attrData ) {
  var options = attrData.scope.attr( el.getAttribute("tooltip") ),
      updateOptions = function(){
        $(el).tooltip( options.attr() )
      }
  options.bind("change", updateOptions);

  $(el).bind("removed", function(){
    options.unbind("change", updateOptions);
  });

  updateOptions();

});

can.view.tagData

@typedef {{}} can.view.tagData

@option {can.view.renderer} [subtemplate] If the special tag has content,
the content can be rendered with subtemplate. For example:

can.view.tag("foo-bar", function(el, tagData){
  $(el).html( tagData.subtemplate(tagData.scope, tagData.options) )
})

@option {can.view.Scope} scope
@option {can.view.Options} options

@matthewp

This comment has been minimized.

Show comment
Hide comment
@matthewp

matthewp Dec 31, 2013

Contributor

Excellent, I wasn't aware can.view.tag could be used directly like this. Using jQuery plugins as an example is great. I've been either using minimal can.Component wrappers or just using helpers to initialize them but can.view.tag is much more appropriate. Excellent documentation.

Contributor

matthewp commented Dec 31, 2013

Excellent, I wasn't aware can.view.tag could be used directly like this. Using jQuery plugins as an example is great. I've been either using minimal can.Component wrappers or just using helpers to initialize them but can.view.tag is much more appropriate. Excellent documentation.

@andykant

This comment has been minimized.

Show comment
Hide comment
@andykant

andykant Jan 2, 2014

Contributor

Sounds awesome, I think in the documentation we definitely need to highlight that there is no live binding with this and that you have to bind/unbind manually.

Contributor

andykant commented Jan 2, 2014

Sounds awesome, I think in the documentation we definitely need to highlight that there is no live binding with this and that you have to bind/unbind manually.

@justinbmeyer

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer Jan 6, 2014

Contributor

I just updated with can.view.attr's docs.

Contributor

justinbmeyer commented Jan 6, 2014

I just updated with can.view.attr's docs.

@cherifGsoul

This comment has been minimized.

Show comment
Hide comment
@cherifGsoul

cherifGsoul Jan 6, 2014

Member

Me like @matthewp I dont know the existance of this 2 features like this CanJS can be easily integrate 3rd party ui components.
Awesome as always

Member

cherifGsoul commented Jan 6, 2014

Me like @matthewp I dont know the existance of this 2 features like this CanJS can be easily integrate 3rd party ui components.
Awesome as always

@justinbmeyer

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer Jan 7, 2014

Contributor

How does document.regsiterElement fit in with can.view.tag?

Contributor

justinbmeyer commented Jan 7, 2014

How does document.regsiterElement fit in with can.view.tag?

@stevenvachon

This comment has been minimized.

Show comment
Hide comment
@stevenvachon

stevenvachon Jan 7, 2014

Contributor

Awesome ideas. I will definitely use can.view.attr for can-transition.

Tip: You can unbind anonymous event callbacks using arguments.callee:

$(el).bind("change", function()
{
    $(this).unbind("change", arguments.callee);
});

and jQuery's $.fn.remove automatically removes all event listeners

Actually, it is needed, because you're removing a listener from a can.Map

Contributor

stevenvachon commented Jan 7, 2014

Awesome ideas. I will definitely use can.view.attr for can-transition.

Tip: You can unbind anonymous event callbacks using arguments.callee:

$(el).bind("change", function()
{
    $(this).unbind("change", arguments.callee);
});

and jQuery's $.fn.remove automatically removes all event listeners

Actually, it is needed, because you're removing a listener from a can.Map

@justinbmeyer

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer Jan 22, 2014

Contributor

This has been merged into minor. #641

Contributor

justinbmeyer commented Jan 22, 2014

This has been merged into minor. #641

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment