Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create can.Component for custom tags #327

Closed
justinbmeyer opened this issue Mar 21, 2013 · 27 comments
Closed

create can.Component for custom tags #327

justinbmeyer opened this issue Mar 21, 2013 · 27 comments
Milestone

Comments

@justinbmeyer
Copy link
Contributor

Background:

I'd like to start making it possible to create custom tags similar to webcomponents.

For example:

<overlay displayWhen="userIsLoggedIn" class="{{classHelper foo}}"><h1> Hello World</h1></overlay>

powered by something like

can.Component("Overlay", {
  template: "TEMPLATEREF",
  "{displayWhen} change": function(displayWhen, ev, display){
    display? this.element.show() : this.element.hide()
  }
})

Considerations:

Merging DOM

How to merge things setup in the "outer" template like:

  • attributes of the custom tag
  • children of the custom tag

with the tag's template?

AMD / Steal nameless modules.

@schovi
Copy link
Contributor

schovi commented Mar 22, 2013

👍
Awesome idea. Still solving how effectively initialize some Controls.

@justinbmeyer
Copy link
Contributor Author

can.Component("Todos",{
  init: function(element, scope){
    scope.attr('todos', [{...}, ....]
  },
  template: "<ul>{{#todos}}<li>{{name}}</li></ul>"
})

@justinbmeyer
Copy link
Contributor Author

            can.Component("tabs",{
                    init: function(element, options){
                        options.attr('panes',[])
                    },
                    addPane: function(pane){
                        if (this.options.panes.length == 0){
                            this.options.select(pane);
                        }
                    this.options.panes.push({
                        selected: false,
                        pane: pane
                    });
                    },
                    select: function(pane){
                        can.each(this.options.panes,function(pane){
                            pane.attr('selected',false)
                        })
                    },
                    template: 
                        '<div class="tabbable">' +
                      '<ul class="nav nav-tabs">' +
                        '{{panes}}'+
                        '<li class="{{#selected}}active{{/}}">'+
                          '<a can-click="select(pane)">{{pane.title}}</a>' +
                        '</li>' +
                      '</ul>' +
                      '<div class="tab-content" can-select="*"></div>' +
                    '</div>'
            });

@matthewp
Copy link
Contributor

Is the goal to 1) provide some today-compatible version of web components, 2) build on top of web components 3) build something that is functionally similar (and if so, to what extent; is it only that this would be custom tag elements?)?

@justinbmeyer
Copy link
Contributor Author

1 + 3.

I think at the start, there will have to be normal elements (to support IE), but with a special attribute name.

Eventually, this can run on web-component technology when it's available in all browsers. The biggest gap right now is mutation events and hooks into custom elements.

@matthewp
Copy link
Contributor

FWIW x-tags (Mozilla's polyfill) uses animationstart as an alternative to mutation events.

@justinbmeyer
Copy link
Contributor Author

What about browsers that do not support animation events?

Sent from my iPhone

On May 17, 2013, at 5:59 PM, Matthew Phillips notifications@github.com wrote:

FWIW x-tags (Mozilla's polyfill) uses animationstart as an alternative to mutation events.


Reply to this email directly or view it on GitHub.

@matthewp
Copy link
Contributor

Should be able to use the old mutation events to get you to IE9. Below that it's probably not practical. I would think it's possible to overload document.createElement and htmlelement.prototype.innerhtml but no one else does that so there's probably a good reason.

The killer feature of web components is the ability to add a tag to your markup and have it just work. In my opinion sacrificing that in order to support older browsers would be a mistake. I'm not sure what the gain is over can.Control if you have to add them through JS.

Another really cool thing about web components is HTML imports which allow you to add components through the link tag. I think there is opportunity here for someone to become the NPM of web components by acting as a repository/CDN for components.

@justinbmeyer
Copy link
Contributor Author

It will just work as long as you are using jQuery or one of the libraries helpers to insert HTML. $().append, $().html, etc. In this way we can support older browsers, and how people typically manipulate the DOM.

Also, with live binding, the amount of DOM manipulation that goes on is exceedingly minimal lately. It's almost always just:

this.element.html( can.view( ... ) )

@justinbmeyer
Copy link
Contributor Author

By this, I mean, we don't really need to support document.createElement and such. We will support that directly in browsers that support it, but most people who use jQuery are accustomed to doing $().html() anyway.

I've talked with James Burke about HTML imports, I don't care for them unless you can build it. We talked about how AMD / require / steal could support loading imports.

But yes, I agree that we need an easy way to share components, but dependency management is really the key because even if web components became popular, they will still have dependencies on libraries.

@justinbmeyer
Copy link
Contributor Author

One more thing ... I don't think that the killer feature of can.Component will be:

the ability to add a tag to your markup and have it just work.

That's the killer feature of web-components as you correctly identified. The advantage of can.Component over can.Control is first that it hooks up 3 extremely common items of a component by convention:

  • can.Control
  • A view-model (which is currently this.options, but this.options is extremely underpowered).
  • A live-bound template

Second, it allows you to "pass" a template to a control. I need a better term for this, but consider this example:

<h2>Things</h2>
<ul can-component="List" data-model="Things">
  {{#items}}
  <li>{{name}}</li>
  {{/items}}
</ul>

The developer doesn't need to do something like this instead:

new List("ul",{
  model: Things,
  view: can.view("list-of-things")
})

The designer can see more of the structure of the page all in one place. This is very nice for basic widget-y type controls that might need a micro template from the user. The user doesn't have to define the template elsewhere.

This breaks down eventually as you want high-level components to carry their template with them. But, that's fine.

cc @shcarrico (I've talked with Stan quite a bit about this)

@steeleprice
Copy link

There is a great need for simplifying the instantiation and communication between Components in a standard way for current functionality. I have spent lots and lots of time architecting and re-architecting how to do this properly with CanJS.

Consider the following architecture:

App (with a component cache)

  • componentA
    • subcomponent1
    • subcomponent2
    • subcomponent3
  • componentB
    • subcomponent2

Currently I have to do this in a less than spectacular way since a "Component" is essentially Construct+Control, one or more Models, one or more templates and a css file. So this defines "Component", though it doesn't address instantiation of a component or Facade and Mediator. For Mediation between Components almost everyone using CanJS correctly recommends using Observe for this but there is no real guidance on where to put them, are they injected into Component or just Observed? We also need Facade for things like security and error management.

App has the responsibility to load Components as needed and for managing their current state (think of it as the smart component cache). App is completely ignorant of what Components are doing but it does know which ones are loaded. Hopefully, most people know Components are not the same as Plugins, Components should be ignorant of the the App and implement a Facade Pattern instead, whereas Plugins are usually extending App and know about (at least some of ) its functionality. Facade and Mediator are missing in CanJS, what's tying all these Components together?

This structure is what CanJS is currently lacking: App->Mediators->Facade->Components->(low level abstractions)
The left four are completely roll-your-own with no clear guidance about how the framework is intended to be used for this. All the low level abstractions are great and working well, adding these or offering clear guidance around how CanJS is best used in this structure will be terrific.

@justinbmeyer
Copy link
Contributor Author

@steeleprice did you read my previous comment about how can.Components main feature is NOT the ability to add custom tags?

@justinbmeyer
Copy link
Contributor Author

@steeleprice anyway you can clean up that post a little to get more to the point. I'm not really sure what you are saying. I'll give you an example:

There is also some cross pollination here with Issue 305 if you want computes directly bound.

What does this have to do with can.Component?

Your post seems to be talking about other issues you are having and not related to this discussion.

@justinbmeyer
Copy link
Contributor Author

@steeleprice btw, we use steal to package components. Packaging is not something that we're looking to solve in CanJS.

@steeleprice
Copy link

I'm looking at steal to see if it can do what I need for loading easier than RequireJS, but that really only deals with the dynamic loading part. Perhaps I am missing the idea of can.Component. I mean if you are going to make can.Component, isn't the idea of a "Component" a package of multiple parts that makes up a usable Widget? WebComponent is pretty specific and if that's what you want to support, call it that. Component is more abstract even though the draft says "Both decorators and custom elements are called components" This is horribly misleading about what the functionality truly is. Packaging is definitely part of the spec. in Part 8: "Custom elements and decorators can be loaded from external files using the link tag" so does this mean can.Component would use steal to load any imports defined in the component? We are talking about 5 pieces designed to be used together to design widgets. This is a huge issue and saying, oh... we use steal is just covering one part of a much bigger issue.

@steeleprice
Copy link

I think I should move this discussion over to the https://forum.javascriptmvc.com/canjs since it's not really an issue other than defining what the functionality of can.Component really should be. There are at least 5 people on the forums trying to solve the same thing (loading, communicating between and packaging what we are all calling components) in 5 different ways.

@justinbmeyer
Copy link
Contributor Author

I agree that any discussion outside the specifics of can.Component should be moved to the forums. But I want to keep this discussion it's functionality. If there are use cases you have around WebComponents, or thoughts, please write them in a forum post.

Here's the functionality of can.Component:

A merger of can.Control, an observable View-Model, and a template, that can be instantiated via custom tags (or other markup).

Think about it this way, how often do you have a can.Control like:

GroupControl = can.Control({
  init: function(){
    this.options.usersLoaded = can.compute(false)
    this.options.showUsers = can.compute(false)
    this.option.users = can.compute()
    this.element.html( INIT_TEMPLATE(this.options) )
  }
})

And how often in a parent control are you doing:

this.element.html(INIT_TEMPLATE)
new Group( this.element.find(".group"), {group: group});

I do this all the time.

I'd like to be able to do something like:

<group>
   <ul {{^showUsers}}style="display:none"{{/showUsers}} class='users'>
        {{#usersLoaded}}
            {{#users}}
                <li {{data "user"}} class='user'>{{name}} <button class='destroy'>X</button></li>
            {{/users}}
            {{^users}}
                <li>No users in group</li>
            {{/users}}
        {{/usersLoaded}}
        {{^usersLoaded}}
            <li>Loading Data</li>
        {{/usersLoaded}}
    </ul>
</group>

And have Group look more like:

can.component("group",{
  template: "<h3>{{group.name}}</h3><div class='content'><content></content>",
  "h3 click": function(){
     if( this.attr('usersLoaded') ){
        this.attr('users', new User.List({groupId: this.attr('group')}, function(){
          self.attr('usersLoaded', true)
        })
     }
     if( ! this.attr('showUsers') ){
       this.attr('showUsers', true)
     } else {
       this.attr('showUsers', false)
     }
  }
})

The special parts of this are:

  1. The group control is created automatically with this.attr(prop) having access to the current context of the template in which it was created. This is how this.attr('group') works.
  2. The template the user provided is merged automatically with the group's template.

And parts not shown:

  1. A can.component is really the view-model, so it will be like an observe, and have setters and such.
  2. Any templated event handlers like "{users} change" will be automatically updated without having to call .on().

What we are going to try to nail down is the very common practice of having a can.Control wire up its view-model and template, and how it is invoked. It's API isn't finalized, but this is the problem we are trying to solve.

@justinbmeyer
Copy link
Contributor Author

I mean, it might be better to break up can.component's parts to be more like how mozilla's x-tags approaches it:

can.component('group',{
  template: "the template",
  model: {
    property: "defaultValue",
    setProperty: function(){}
  },
  events: {
    "h3 click": function(){
      this.model.property
    }
  }
})

@matthewp
Copy link
Contributor

Why put the event functions on an events property?

@steeleprice
Copy link

Thank you very much for this great explanation. This is indeed exactly what I am trying to resolve in a better way right now with a single caveat, the large scale Application. It's Applications with High Level (i.e. pages) components and several reusable components within.

Srchr is a great example, but it has no concept of things like authorization or nested widgets which are prevalent in real-world line of business applications.

Application is the root, and functionality is loaded dynamically via routing or history. The Engine of Application State is the issue at hand not just control state. When I have my "App" at the root just refreshing the browser is going to cause all state to reload (which it should). Which components are loaded as well as what state those components are in starts to become a very complex issue. Using Srchr as a sample, some state gets pushed into localstorage (history) and we are mixing local state with remote state (the stuff we search for).

For mobile we have a need potentially for offline use, the App (say wrapped in Cordova) is there and items are cached locally until there is a connection then pushed up to the server; this means we need both a local Model and a remote one for the Component unless we start hacking at the current Model structure to override what create/update does.

These are the difficult things I would really like to see get addressed in a more standard way for CanJS and I think Component can solve a lot of this if it has the concept of how to do it even if its a plugin.

@steeleprice
Copy link

Maybe in addition to can.Component we need can.Mediate that is an Observe used specifically for communicating between Components and can.Facade which handles swapping out Model storage for local vs remote usage patterns similar to how Fixture is used for testing.

@justinbmeyer
Copy link
Contributor Author

So, I was kinda thinking that your comments were more about high-level application architecture instead of can.component. Can you please sum them up in a forum post and remove some of the comments here to keep it can.component focused?

@justinbmeyer
Copy link
Contributor Author

@matthewp to better separate the model code from event code. I think it might make things easier to read.

@hyusetiawan
Copy link

are there any updates on this effort? Or has the discussion moved somewhere else?
Would love to know the progress on this and chip in a cent or two

@daffl
Copy link
Contributor

daffl commented Sep 17, 2013

There is a forum discussion and you can try the implementation out in the canComponent branch.

@daffl
Copy link
Contributor

daffl commented Oct 11, 2013

Closing this as it has been merged into master yesterday. Release pending for next week.

@daffl daffl closed this as completed Oct 11, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants