7 min read

This post describes how to build a dropdown menu using Can.js. In this example, we will build a dropdown menu of names. If you’d like to see the complete example of what you’ll be building, you can check it out here.

Setup

The very first thing you will need to do is to import Can.js and jQuery.

<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://rawgit.com/canjs/canjs/v3.0.0-pre.6/dist/global/can.all.js"></script>

Our First Model

To make a model, you use can DefineMap.

If you’re following along using Code Pen or JSBin in the js tab, type the following piece of code[ZB1] :

var Person = can.DefineMap.extend({
   id: "string",
   name: "string",
});

Here, we have a model named Person that defined the properties of id and name as string types. You can read about the different types that Can.js has here: https://canjs.com/doc/can-map-define._type.html.

Can.js 3.0 allows us to declare types in two different ways. We could have also written the following piece of code[ZB2] :

var Person = can.DefineMap.extend({
   id: {
       type: "string",
   },
   name: {
       type: "string",
   },
});

I tend to use the second syntax only when I have other settings I need to define on a particular property. The short hand of the first way makes things a bit easier.

Getting it on the Page

Since we’re building a dropdown, we will most likely want the user to be able to see it. We’re going to use can.stache to help us with this. In our HTML tab, write the following lines of code:

<script type='text/stache' id='person-template'>
<h1>Person Template</h1>
<input placeholder="{{person.test}}"/>
</script>

The {{person.test}} is there, so you can see if you have it working. We’ll add a test property to our model.

var Person = can.DefineMap.extend({
   id: "string",
   name: "string",
   test: {
       value: 'It's working!'
   }
});

Now, we need to create a View Model. We’re going to use Define Map again.

Add the following to your js file:

var PersonVM = can.DefineMap.extend({
   person: {Value: Person},
});

You might notice that I’m using Value with a capitol “V”. You have the option of using both value and Value. The difference is that Value causes the new to be used.

Now, to use this as our View Model, you’ll need to add the following to your js tab.

can.Component.extend({
   tag: 'person',
   view: can.stache.from('person-template'),
   ViewModel: PersonVM
});

var vm = new PersonVM();

var template = can.stache.from('person-template')
var frag = template(vm);

document.body.appendChild(frag);

The can.stache.from (‘person-template’) uses the ID from our script tag. The tag value person is so that we can use this component elsewhere, like <person>.

If you check out the preview tab, you should see a header followed by an input box with the placeholder text we set. If you change the value of our test property, you should see the live binding updating.

Fixtures

Can.js allows us to easily add fixtures so we can test our UI without needing the API set up. This is great for development as the UI and the API don’t always sync up in terms of development. We start off by setting up our set Algebra.

Put the following at the top of your js tab:

var personAlgebra = new set.Algebra(
   set.props.id('id'),
   set.props.sort('sort')
);

var peopleStore = can.fixture.store([
   { name: "Mary", id: 5 },
   { name: "John", id: 6 },
   { name: "Peter", id: 7 }
], personAlgebra);

The set.Algebra helps us with some things. The set.props.id allows us to change the ID property. A very common example is that Mongo uses _id. We can easily change the ID property to map responses from the server with _id to our can model’s id.

In our fixture, we are faking some data that might already be stored in our database. Here, we have three people that have already been added.

We need to add in a fixture route to catch our requests so we can send back our fixture data instead of trying to make a call to our API:

can.fixture("/api/people/{id}", peopleStore);

Here, we’re telling can to use the people store whenever we have any requests using /api/people/{id}.

Next, we will need to tell can.js how to use everything we just set up. We’re going to use can-connect for that.

Add this to your js tab:

Person.connection = can.connect.superMap({
   Map: Person,
   List: Person.List,
   url: "/api/people",
   name: "person",
   algebra: personAlgebra
});

Does it work?

Let’s see if it’s working. We’ll write a function in our viewModel that allows us to save. Can-connect comes with some helper functions that allow us to do basic CRUD functionality. Keeping this in mind, update your Person View Model as follows:

var PersonCreateVM = can.DefineMap.extend({
   person: {Value: Person},
   createPerson: function(){
       this.person.save().then(function(){
           this.person = new Person();
       }.bind(this));
   }
});

Now, we have a createPerson function that saves a new person to the database and updates the person to be our new person.

In order to use this, we can update our input tag to the following:

<input placeholder="Name"
   {($value)}="person.name"
   ($enter)="createPerson()"/>

This two-way binds the value of the input to our viewModel. Now, when we update the input, person.name also gets updated, and when we update person.name, the input updates as well.

($enter)=createPerson() will call createPerson whenever we press Enter.

Populating the Select

Now that we can create people and save them, we should be able to easily create a list of names. Since we may want to use this list of names at many places in our app, we’re making the list its own component. Add this to the HTML tab.

First, we will create a view model for our People. We’re going to end up passing our people into the component. This way, we can use different people, depending on where this dropdown is being used.

var PeopleListVM = can.DefineMap.extend({
   peoplePromise: Promise,
});

can.Component.extend({
   tag: "people-list",
   view: can.stache.from("people-list-template"),
   ViewModel: PeopleListVM
});

Then update your HTML with a template. Since peoplePromise is a Promise, we want to make sure it is resolved before we populate the select menu. We also have the ability to check isRejected, and isPending. value gives us result of the promise. We also use {{#each}} to cycle through each item in a list.

<script type='text/stache' id='people-list-template'>
   {{#if peoplePromise.isResolved}}
       <select>
       {{#each peoplePromise.value}}
           <option>{{name}}</option>
       {{/each}}
       </select>
   {{/if}}
</script>

Building Blocks

We can use these components, such as building blocks, in various parts of our app. If we create an app view model, we can put people there. We are using a getter in this case to get back a list of people. .getList({}) comes with DefineMap. This will return a promise.

var AppVM = can.DefineMap.extend({
   people: {
       get: function(){
           return Person.getList({});
       }
   }
});

We will update our HTML to use these components. Now, we’re using the tags we set up earlier. We can use the following to pass people into our people-list component: <people-list {people-promise}=”people”/>. We can’t use camel case in our stache file, so we will use hypens. can.js knows how to convert this into camel case for us.

<script type='text/stache' id='names-template'>
<div id="nameapp">
       <h1>Names</h1>
       <person-create/>
       <people-list {people-promise}="people"/>
</div>
</script>

Update the vm to use the app view model instead of the people view model.

var vm = new AppVM();

var template = can.stache.from("app-template")
var frag = template(vm);

document.body.appendChild(frag);

And that’s it! You should have a drop-down menu that updates as you add more people.

About the author

Liz Tom is a developer at Bitovi in Portland, OR, focused on JavaScript. When she’s not in the office, you can find Liz attempting parkour and going to check out interactive displays at museums.

LEAVE A REPLY

Please enter your comment!
Please enter your name here