Example 2 - The Sandlot
Rob Attorri - 21 Feb 2017
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;
}
}