CanJS Widgetry #2 - FIle Navigator

Made a video showing how to create a file navigation widget:

Walk through this here: https://gist.github.com/justinbmeyer/cab04443d02f3ca36f126ae54ab276b8

Post other training topics you’d like to see here: https://github.com/bitovi/training/issues

1 Like

Here’s the beginner guide: https://gist.github.com/justinbmeyer/adc9797a8ac1713b9d6d21d33a44e829

We are working on something like this - a tree component.
The difference is that we should be able to add folders (to the root or the child folders).

But we can’t make it work, if we add a folder to an existing folder.
newFolder.save() keeps pushing the item to the EntitiesList instead of the current folder.

var self = this;

Entity.getList({parentId: "0"}).then(function(entities){
    self.entities = entities;
})

// ...

var newFolder = new Entity({
	parentId : self.currentFolder.id,
	title : 'New Folder',
	type : 'folder',
	hasChildren : false
});

newFolder.save(successData => {
	self.currentFolder.children.push(successData);
}, errorData => {
	console.error(errorData);
});

We’ve tried something like this:
self.currentFolder.children.push(newFolder);

But the new folder is also added to the EntitiesList. I’ve tried doing a pop() on the list and it works, but that doesn’t feel correct.

I think we need something like canMergeBehaviour.
But I can’t wrap my head around it…

1 Like

Do you have a JSBin?

If you are mimicing the behavior of the “advanced” tutorial, where is children coming from?

If I were doing the advanced tutorial, you shouldn’t have to use .push at all … can-connect/real-time should handle adding new entries into their appropriate list automatically.

That’s what it should be doing. it should add them to the entities list auto-magically. Why isn’t this the desired behavior?

It adds it at the end of the original list, and not the selected folder’s children.

What do you mean by “original list”? If the algebra is right, it should work out. Checkout some debugging tips on algebras: http://canjs.com/doc/can-set.html#SolvingCommonIssues

The original list is this:

Entity.getList({parentId: "0"}).then(function(entities){
    self.entities = entities;
});

If we create a new folder like this:

var newFolder = new Entity({
	parentId : self.currentFolder.id,
	title : 'New Folder',
	type : 'folder',
	hasChildren : false
});

The newFolder doesn’t know it should get pushed inside currentFolder.children but it get’s pushed inside self.entities.

I will have a look at the Algebra, but I found it very confusing.

This seems to work: http://jsbin.com/vasixip/edit?html,js,output

I HACKED in a makeFolder method:

makeFolder: function(parentId){
    var newFolder = new Entity({
      parentId : parentId,
      name : 'New Folder',
      type : 'folder',
      hasChildren : false
    }).save()
  }

If you open some folders, and write the ID of an openned folder, then hit enter, the new folder will appear in the list in the correct spot.

I had to change your new Entity code to use"name" instead of “title”.

If you want the new folders to show up in the right spot, you’ll need a configured set.prop.sort. You’ll also need to change the .getList({ sort: ... }) to include it.

There is no currentFolder.children as far as I can tell. Where do you see that?

// Stores the next entity id to use. 
var entityId = 1;

// Returns an array of entities for the given `parentId`.
// Makes sure the `depth` of entities doesn't exceed 5.
var makeEntities = function(parentId, depth){
  if(depth > 5) {
    return [];
  }
  // The number of entities to create.
  var entitiesCount = can.fixture.rand(10);

  // The array of entities we will return.
  var entities = [];

  for(var i = 0 ;  i< entitiesCount; i++) {

    // The id for this entity
    var id = ""+(entityId++),
        // If the entity is a folder or file
        isFolder = Math.random() > 0.3,
        // The children for this folder.
        children = isFolder ? makeEntities(id, depth+1) : [];

    var entity = {
      id: id,
      name: (isFolder ? "Folder" : "File")+" "+id,
      parentId: parentId,
      type: (isFolder ? "folder" : "file"),
      hasChildren: children.length ? true : false
    };
    entities.push(entity);

    // Add the children of a folder
    [].push.apply(entities,  children)

  }
  return entities;
};

// Make the entities for the demo
var entities = makeEntities("0", 0);

// Add them to a client-like DB store
var entitiesStore = can.fixture.store(entities);

// Trap requests to /api/entities to read items from the entities store.
can.fixture("/api/entities", entitiesStore);

// Make requests to /api/entities take 1 second
can.fixture.delay = 1000;

var Entity = can.DefineMap.extend({
  id: "string",
  name: "string",
  parentId: "string",
  hasChildren: "boolean",
  type: "string",
  makeFolder: function(parentId){
    var newFolder = new Entity({
      parentId : parentId,
      name : 'New Folder',
      type : 'folder',
      hasChildren : false
    }).save()
  }
});

can.connect.baseMap({
  Map: Entity,
  url: "/api/entities"
});


var folder = new Entity({
  id: "0",
  name: "ROOT/", 
  hasChildren: true,
  type: "folder"
});


var FolderVM = can.DefineMap.extend({                    // ADDED
  folder: Entity,
  entitiesPromise: {
    value: function(){
      return Entity.getList({parentId: this.folder.id});
    }
  },
  isOpen: {type: "boolean", value: false},
  toggleOpen: function(){
    this.isOpen = !this.isOpen;
  }
});

can.Component.extend({                       // ADDED
  tag: "a-folder",
  ViewModel: FolderVM,
  view: can.stache.from("folder-template")
});


var template = can.stache.from("app-template"),
    frag = template(folder);                   // CHANGED

document.body.appendChild( frag );

No there isn’t, that was in our component

No there isn’t, that was in our component

Ok, how does .children get created?

They come from the backend (fixture). We don’t have lazy-loading, we receive the complete three the first time from the backend.

So I would either:

var Entity = can.DefineMap.extend("Entity",{
  id: "string",
  name: "string",
  parentId: "string",
  hasChildren: "boolean",
  type: "string",
  children: [{
    type: function(entity){
      return new Entity(entity)
    }
  }],
  isOpen: {type: "boolean", value: false},   // ADDED
  toggleOpen: function(){                    // ADDED
    this.isOpen = !this.isOpen;
  }
});

This should change to something more like:

var Entity = can.DefineMap.extend("Entity",{
  id: "string",
  name: "string",
  parentId: "string",
  hasChildren: "boolean",
  type: "string",
  children: {
    type: function(entities){
      if(! (entities instanceof Entity.List) {
        return new Entity.List(entities);
      } else {
        return entities;
      }
    },
    set: function(entities){
      entities.__listSet = {parentId: this.id}
    }
  },
  isOpen: {type: "boolean", value: false},   // ADDED
  toggleOpen: function(){                    // ADDED
    this.isOpen = !this.isOpen;
  }
});

Entity.List = DefineList.extend({
  "#": {
    type: Entity
  },
  __listSet: "any"
})

For 100% correctness, I’d probably have a setter on the id property that changed children.__listSet if changed.

id: {
  set: function(id){
    if(this.children) {
      this.childen.__listSet = {parentId: id}
    }
    return id;
  }
}

Essentially, setting __listSet informs the set algebra what set that list belongs to. It uses that to match any created or updated instances. If you are going to be creating and updating folders, especially if you have a real-time connection, it is probably worth doing.

Docs around the listSet: https://canjs.com/doc/can-connect/base/base.listSet.html

Thx, Justin. We will have a look into this.
Things are getting complicated.

Things are getting complicated.

I’m sorry you feel that way. Like I said, you can remove the real-time behavior and you’ll have to handle updating things on your own.

Or, you can decorate your lists in a way where CanJS knows how to update them for you. Conceptually, you are simply adding a property that describes what belongs in the list. Specifically, a list has every entity where it’s parentId is some value.

Imo, it’s not that complex related to the benefit it provides (automatic real-time behavior).

this.childen.__listSet = {parentId: id}

So if a list has a __listSet of {parentId: “5”}, entities like:

{id: 66, name: "something", parentId: "5"}
{id: 67, name: "something else", parentId: "5"}
{id: 68, name: "something new", parentId: "5"}

belong in that list.

{id: 76, name: "something", parentId: "6"}
{id: 77, name: "something else", parentId: "7"}
{id: 78, name: "something new", parentId: "8"}

do not.

real-time monitors all creates, updates, and deletes of data globally and updates lists to look right, in a memory safe way.

1 Like

Thx for the response!
The second option (with the __listSet) did the trick.
It was important though that our initial request passed the set as well:

EntityModel.getList({where : {parentId : 0}});

1 Like