Example 2 - The Sandlot

Rob Attorri - 21 Feb 2017

Example 2

What is this?

This app demonstrates how to create a reusable component and how to inject it into the browser. This app displays a batting order for a baseball team using a #player-card component. This component displays information relating to each player on a baseball team – picture, name, position, and handedness. A #player-card can also be configured with additional functionality using optional tags. The app also shows some of the ways sort can be used, and how to choose specific records out of a list of records.

You can play with this example in your browser here.

Page Layout

Containers

I want to break the page down into two sections: one for the active lineup and the bench players, and one for players on injured reserve.

commit @browser
  [#div class:"container" children:
    [#div #active class:"lineup" text:"Batting Order"]
    [#div #inactive class:"lineup" text:"Bench"]]
  [#div #IR class:"lineup IR" text:"Injured Reserve"]

Active Lineup

The first column drawn should be the active batting order. While I could have specified all the text and contents of each player in this block, note that I used two tags, #player-card and #moveable, whose contents and behavior get defined later. Since every player listed is going to have the same information, #player-card lets me drop the tag in and define all the HTML and content later, which is done in the Player Cards section after this one.

search @session @browser
  player = [#player not(#bench) not(#injured) lineup]
  active = [#active]

bind @browser
  active.children += [#div #lineup children: [#player-card #moveable player sort: lineup]]

Bench Players

These are the bench players. Note that #player-card is used again, saving me the work of having to define all the same html and content that’s used in the active lineup. The big difference here is the ordering of the players. Whereas the players in the lineup have a deliberate and specific order (which makes sense for things like a batting order), I don’t need a specific order for the bench players. I do, however, want to know how many there are and make sure they render in the same order each time, so I sort by the player variable to get an index, n. Each player gets assigned an index, which is how I sort the bench players, and also set player.lineup to that index so they get numbered 1, 2, 3, and so on.

search @session @browser
  player = [#player #bench lineup]
  inactive = [#inactive]
  n = sort[value:player]

bind @session @browser
  player.lineup := n
  inactive.children += [#div #lineup children: [#player-card #benched sort:n player]]

Injured Reserve

These are the injured reserve players. This section is functionally identical to the bench player section, except it gets merged into the #IR section on the page. There’s only one player on the IR and no functionality exists to move him elsewhere, because he is a jerk.

search @session @browser
  player = [#player #injured not(#bench) lineup]
  IR = [#IR]
  n = sort[value:player]

bind @session @browser
  player.lineup := n
  IR.children += [#div #lineup children: [#player-card sort:n player]]

Player Cards

This is where the magic happens. Every time this finds #player-card in the browser, it will make a #div for each player and spit out a player card. It checks to see if the player has a nickname as well, and just leaves a space between the first and last names if there isn’t. It’s worth mentioning that because I want this to be as general a template as possible, a handful of architectural decisions were made to facilitate that. In the sections that draw each of the three lists, I use the search parameters there to select the specific players I want and inject those player cards. In the Roster Data, all the players have a lineup attribute even though the bench and IR players have an empty string as the value. If you’re good at predicting which reusable parts you’ll need, you can implement these sorts of things from the outset, or you can figure out where they need to be as you go along and refactor them into your code as you work.

search @browser @session
  container = [#player-card player]
  player = [#player firstname lastname position bats lineup photo]
  middle = if player.nickname then " \"\" "
           else " "

bind @browser
  container <- [#div player class: "lineup-position" children:
    [#div class: "bat-number" text: lineup]
    [#div class: "player-info" children:
      [#img class: "player-photo" src: photo]
      [#div class: "player-text" children:
        [#div class: "name-line" children:
          [#div class: "position" text: position]
          [#div class: "first-name" text: firstname]
          [#div class: "middle-name" text: middle]
          [#div class: "last-name" text: lastname]]
        [#div class:"bat-hand" text:"Bats: "]]]]

Buttons

Move Up

The active roster also features a cool way to use tags. Since I want to be able to reorder my active lineup, each #player-card there is also tagged #moveable. This checks to see if a #player-card is #moveable and adds a child #div to it with a #move-up-btn, unless that player is already at the top of roster and can’t move up any further.

search @browser @session
  container = [#div #moveable player]
  not(player.lineup = 1)

bind @browser
  container.children += [#div #move-up-btn player class: "ion-chevron-up"]

It’s like hashtag inception. Just as the #moveable tag let Eve know to add a #move-up-btn, this block adds another layer of functionality by giving that button some behavior. In this case, when a #move-up-btn is clicked, Eve captures who the specific clicked-player is, finds whoever is one space above them in the lineup, and switches their places.

search @browser @event @session
  [#click element: [#move-up-btn player: clicked-player]]
  above-clicked = [#player lineup: clicked-player.lineup - 1]

commit
  clicked-player.lineup := clicked-player.lineup - 1
  above-clicked.lineup := above-clicked.lineup + 1

Move Down

This block is the mirror of the move up buttons. If a #player-card is #moveable, and not at the bottom of the lineup, which is what the count is checking, then a child #div gets added with a #move-down-btn.

search @browser @session
  container = [#div #moveable player]
  not(player.lineup: count[given: [#player not(#bench) not(#injured)]])

bind @browser
  container.children += [#div #move-down-btn player class: "ion-chevron-down"]

Once again the reciprocal of the previous section, when a #move-down-btn is clicked, Eve captures the specific clicked-player, finds the player one space below them in the lineup, and switches their places.

search @browser @event @session
  [#click element:[#move-down-btn player: clicked-player]]
  below-clicked = [#player lineup: clicked-player.lineup + 1]

commit
  clicked-player.lineup := clicked-player.lineup + 1
  below-clicked.lineup := below-clicked.lineup - 1

Remove from Lineup

If a player is #moveable, it means they’re in the active lineup and should be able to be removed from the active lineup and sent to the bench. This simply looks for any #moveable players and adds a #deactivate button to them.

search @browser @session
  container = [#div #moveable player]

bind @browser
  container.children += [#div #deactivate player class: "ion-log-out"]

This gives #deactivate its behavior. The clicked-player is sent to the bench and is stripped of their lineup order. All the players who were below them on the lineup get moved up a spot so that there aren’t gaps in the numbering.

search @browser @event @session
  [#click element: [#deactivate player: clicked-player]]
  players-below = [#player lineup > clicked-player.lineup]

commit
  clicked-player += #bench
  clicked-player.lineup := ""
  players-below.lineup := players-below.lineup - 1

This block takes care of an edge case for #deactivate. Because the previous block finds all the players-below the clicked-player, if the player is at the bottom of the lineup then there are no players-below and the search block fails, preventing the button from doing anything. This makes sure you can move the ninth batter off the lineup onto the bench.

search @browser @event @session
  [#click element: [#deactivate player: clicked-player]]

commit
  clicked-player += #bench
  clicked-player.lineup := ""

Add to Lineup

This adds an #activate button to the bench players so we can add them to the active lineup, but only if there are fewer than nine batters already in the lineup.

search @browser @session
  container = [#div #benched player]
  active-players = if c = count[given: [#player not(#bench) not(#injured)]] then c else 0
  active-players != 9

bind @browser
  container.children += [#div #activate player class:"ion-log-in"]

This gives #activate its behavior. When an #activate button is clicked, Eve finds out how many active players are in the lineup then adds the clicked-player and assigns them the next number in the lineup.

search @browser @event @session
  [#click element: [#activate player:clicked-player]]
  active-players = if c = count[given:[#player not(#bench) not(#injured)]] then c else 0
  active-players != 9

commit
  clicked-player -= #bench
  clicked-player.lineup := active-players + 1

Roster Data

Our sample roster data.

commit
  [#player firstname:"Kenny" lastname:"DeNunez" position:"P" bats:"R" lineup:3 photo:"http://i.imgur.com/kaRnA7R.png"]
  [#player firstname:"Hamilton" lastname:"Porter" nickname:"Ham" position:"C" bats:"R" lineup:1 photo:"http://i.imgur.com/7C678n2.jpg"]
  [#player firstname:"Timmy" lastname:"Timmons" position:"IF" bats:"R" lineup:8 photo:"http://i.imgur.com/JLHupGR.png"]
  [#player firstname:"Bertram" lastname:"Grover Weeks" position:"IF" bats:"R" lineup:2 photo:"http://i.imgur.com/mfIMIy6.png"]
  [#player firstname:"Alan" lastname:"McClennan" nickname:"Yeah-Yeah" position:"IF" bats:"R" lineup:6 photo:"http://i.imgur.com/ZyLzCDn.jpg"]
  [#player firstname:"Benny" lastname:"Rodriguez" nickname:"The Jet" position:"IF" bats:"S" lineup:9 photo:"http://i.imgur.com/UOywuNz.jpg"]
  [#player firstname:"Scott" lastname:"Smalls" position:"OF" bats:"R" lineup:5 photo:"http://i.imgur.com/eBZ2m17.jpg"]
  [#player firstname:"Michael" lastname:"Palledorous" nickname:"Squints" position:"OF" bats:"R" lineup:4 photo:"http://i.imgur.com/q8KKRz6.jpg"]
  [#player firstname:"Tommy" lastname:"Timmons" nickname:"Repeat" position:"OF" bats:"R" lineup:7 photo:"http://i.imgur.com/QPzSCGy.png"]
  [#player #bench firstname:"Thelonius" lastname:"Mertle" position:"IF" bats:"L" lineup:"" photo:"http://i.imgur.com/XDA0ftH.jpg"]
  [#player #bench firstname:"George Herman" lastname:"Ruth" nickname:"Babe" position:"P" bats:"L" lineup:"" photo:"http://i.imgur.com/kep7Unm.jpg"]
  [#player #bench firstname:"Hercules" lastname:"Mertle" nickname:"The Beast" position:"PR" bats:"S" lineup:"" photo:"http://i.imgur.com/WOwMn5c.jpg"]
  [#player #injured firstname:"" lastname:"Phillips" position:"IF" bats:"R" lineup:"" photo:"http://i.imgur.com/Qvxya5C.jpg"]

Styles

The app needs a good bit of CSS to organize the page sections and various buttons as well as stylize the player cards.

.container  {
  display: flex;
  flex-direction: row;
  user-select: none;
  font-size: 20px;
  text-transform: uppercase;
  text-align: center;
  overflow: scroll;
  height: 1000px;
  border-bottom: 1px solid #555;
  flex: 1 1 auto;
}

.IR {
  width: 515px;
  margin-top: 30px;
  flex: 1 0 auto;
}

.lineup {
  display: flex;
  flex-direction: column;
  font-size: 20px;
  text-transform: uppercase;
  text-align: center;
  margin-right: 50px;
  position: relative;
  flex: 1 0 515;
  width: 515px;
  min-width: 515px;
  overflow: scroll;
}

.lineup-position {
  list-style: none;
  display: flex;
  flex-direction: row;
  align-items: center;
  margin-top: 15px;
  position: relative;
}

.bat-number {
  order: 1;
  font-size: 40px;
  font-weight: bold;
  color: #b4b4b4;
  margin-right: 15px;
  width: 50px;
}

.player-info {
  display: flex;
  flex-direction: row;
  margin: 0px 0px;
  padding: 0px 0px 0px 0px;
  height: 85px;
  width: 450px;
  background: #ffffff;
  border: 1px solid #555;
  border-radius: 8px;
  order: 2;
  overflow: hidden;
}

.name-line {
  display: flex;
  flex-direction: row;
  margin: 10px 10px;
}

.position {
  font-size: 14px;
  font-weight: bold;
  margin-right: 8px;
  padding-top: 2px;
  height: 16px;
}

.first-name {
  font-size: 16px;
  text-transform: uppercase;
  height: 16px;
}

.middle-name {
  font-size: 16px;
  text-transform: uppercase;
  height: 16px;
  font-weight: 600;
  white-space: pre;
}

.last-name {
  font-size: 16px;
  text-transform: uppercase;
  height: 16px;

}

.player-photo {
  height: 85px;
  width: 85px;
  border-right: 1px solid #555;
}

.bat-hand {
  font-size: 14px;
  height: 14px;
  text-align: left;
  margin: 10px;
}

.ion-chevron-up {
  position: absolute;
  top: 2px;
  right: 15px;
  font-size: 24px;
  cursor: pointer;
}

.ion-chevron-down {
  position: absolute;
  bottom: 2px;
  right: 15px;
  font-size: 24px;
  cursor: pointer;
}

.ion-log-out {
  position: absolute;
  right: 15px;
  color: #e65b3c;
  font-size: 24px;
  cursor: pointer;
}

.ion-log-in {
  position: absolute;
  right: 15px;
  transform: scaleX(-1);
  color: #009ee0;
  font-size: 24px;
  cursor: pointer;
}
@media (max-width: 1848px) {

.container  {
  flex-direction: column;
  border-bottom: none;
  height: auto;
  flex: 0 0 auto;
}

.IR {
  width: 515px;
  margin-top: 0px;
}

.lineup {
  display: flex;
  font-size: 18px;
  margin-right: 0px;
  flex: 0 0 auto;
  width: 415px;
  min-width: 415px;
  min-height: 100px;
  margin-bottom: 20px;
}

.lineup-position {
  margin-top: 12px;
}

.bat-number {
  font-size: 30px;
  margin-right: 15px;
  width: 50px;
}

.player-info {
  height: 45px;
  width: 400px;
}

.name-line {
  margin: 5px 8px;
}

.position {
  font-size: 10px;
  margin-right: 8px;
}

.first-name {
  font-size: 12px;
}

.middle-name {
  font-size: 12px;
}

.last-name {
  font-size: 12px;
}

.player-photo {
  height: 45px;
  width: 45px;
}

.bat-hand {
  font-size: 10px;
  margin:  5px 8px;
}

.ion-chevron-up {
  top: 0px;
  right: 15px;
  font-size: 16px;
}

.ion-chevron-down {
  bottom: 0px;
  right: 15px;
  font-size: 16px;
}

.ion-log-out {
  right: 15px;
  font-size: 16px;
}

.ion-log-in {
  right: 15px;
  font-size: 16px;
}

}
@media (max-width: 1200px) {

.container  {
  flex-direction: column;
  border-bottom: none;
  height: auto;
  flex: 0 0 auto;
}

.IR {
  width: 515px;
  margin-top: 0px;
}

.lineup {
  display: flex;
  font-size: 18px;
  margin-right: 0px;
  flex: 0 0 auto;
  width: 100%;
  min-width: 150px;
  min-height: 100px;
  margin-bottom: 20px;
}

.lineup-position {
  margin-top: 12px;
}

.bat-number {
  font-size: 30px;
  margin-right: 0px;
  width: 0px;
}

.player-info {
  height: 55px;
  width: 100%;
}

.name-line {
  margin: 2px 8px;
  margin-top: 5px;
  flex-wrap: wrap;
}

.position {
  font-size: 8px;
  margin-right: 5px;
}

.first-name {
  font-size: 10px;
}

.middle-name {
  font-size: 10px;
}

.last-name {
  font-size: 10px;
}

.player-photo {
  height: 55px;
  width: 55px;
}

.bat-hand {
  font-size: 10px;
  margin:  0px 8px;
}

.ion-chevron-up {
  top: 7px;
  right: 8px;
  font-size: 12px;
}

.ion-chevron-down {
  bottom: 6px;
  right: 8px;
  font-size: 12px;
}

.ion-log-out {
  right: 8px;
  font-size: 12px;
}

.ion-log-in {
  right: 8px;
  font-size: 12px;
}

}