


This tutorial is part of the Building Your Startup With PHP series on Envato Tuts+. In this series, I'm guiding you through launching a startup from concept to reality using my Meeting Planner app as a real-life example. Every step along the way, I'll release the Meeting Planner code as open-source examples you can learn from. I'll also address startup-related business issues as they arise.
Leveraging Bootstrap, Ajax, and jQuery
Through our startup series, Meeting Planner and Simple Planner have evolved an incredibly long way. Recently, I've been trying to tune into detailed areas to make using the service to schedule meetings an even higher degree of easy.
If you remember our recent episode Building Your Startup: Dynamic Ajax Forms for Scheduling (Envato Tuts+), you know how helpful Ajax and jQuery can be to usability. Making scheduling interactive with Ajax has transformed the usability of the site.
Next, I wanted to improve one pain point that I've run into using the service. Frankly, it's been time-consuming when sending out invitations to suggest multiple options for dates and times. Every time I send a meeting invitation for my own startup, I had to manually create two or three date/time options—and it was kind of annoying.
In today's episode, I'm going to guide you through how I made it simple to schedule a meeting with several related dates and times in a single step. Specifically, I'll describe how I used Bootstrap, Ajax and jQuery to solve the problem of choosing dates and times.
Bootstrap made it easy to design the feature for desktop, tablet and mobile devices, and Ajax and jQuery made it fast and interactive.
If you haven't tried out Meeting Planner or Simple Planner yet, go ahead and schedule your first meeting. Look for the topic of this tutorial as you choose your date and time options.
I do participate in the comment threads below, so tell me what you think! You can also reach me on Twitter @lookahead_io. I'm especially interested if you want to suggest new features or topics for future tutorials.
As a reminder, all of the code for Meeting Planner is written in the Yii2 Framework for PHP. If you'd like to learn more about Yii2, check out our parallel series Programming With Yii2.
Designing the Solution



Using Meeting Planner over time, I'd regularly wanted a way to create a series of dates and times in a row, like the next three days at 8:30 am or the next three weeks on Wednesday at 7 pm. It just makes it easier to schedule with people when you have multiple options for when you're going to meet.
As I delved into deeper polishing of the user interface, I finally had my own time to focus on this issue. Before I wrote any code, I decided to loosely sketch above what I wanted.
I decided to create a repeat quantity, such as the next three or five, and a repeat unit, such as hours, days, or weeks.
In other words, let's say I'm inviting the editorial droid assistant Tom McFarlin to coffee and want to offer any of the next three mornings, then I choose two and days to repeat after my chosen day.
Keeping It Simple
I didn't want people to always be confronted with a complex form just to schedule a meeting, so I separated the date time repetition feature with an advanced options link shown below. Touching or clicking this link opens the form shown below:



Getting Started Writing Code
To design the form to work with both desktop and mobile devices, I leveraged Bootstrap. Essentially, I created multiple rows for the form with various column widths that collapse on mobile. Let's look.
Most of the HTML magic happens here, in /frontend/views/meeting-time/_form.php. First, here's the row with the Date, Time, Duration and advanced options link:
1 |
<div class="meeting-time-form"> |
2 |
<div class="row"> |
3 |
<div class="col-xs-12 col-md-4 col-lg-3"> |
4 |
<?php $form = ActiveForm::begin();?> |
5 |
<?= Html::activeHiddenInput($model, 'url_prefix',['value'=>MiscHelpers::getUrlPrefix(),'id'=>'url_prefix']); ?> |
6 |
<?= Html::activeHiddenInput($model, 'tz_dynamic',['id'=>'tz_dynamic']); ?> |
7 |
<?= Html::activeHiddenInput($model, 'tz_current',['id'=>'tz_current']); ?> |
8 |
<strong><?php echo Yii::t('frontend','Date') ?></strong> |
9 |
<div class="datetimepicker-width"> |
10 |
<?= DateTimePicker::widget([ |
11 |
'model' => $model, |
12 |
'attribute' => 'start', |
13 |
'template' => '{input}{button}', |
14 |
//'language' => 'en',
|
15 |
'size' => 'ms', |
16 |
'clientOptions' => [ |
17 |
'autoclose' => true, |
18 |
'format' => 'M d, yyyy', |
19 |
'todayBtn' => true, |
20 |
//'pickerPosition' => 'bottom-left',
|
21 |
'startView'=>2, |
22 |
'minView'=>2, |
23 |
// to do - format three day ahead
|
24 |
'initialDate'=> Date('Y-m-d',time()+3600*72), |
25 |
]
|
26 |
]);?></div> |
27 |
<p></p>
|
28 |
</div>
|
29 |
<div class="col-xs-12 col-md-4 col-lg-3"> |
30 |
<strong><?php echo Yii::t('frontend','Time') ?></strong> |
31 |
<div class="datetimepicker-width"> |
32 |
<?= DateTimePicker::widget([ |
33 |
'model' => $model, |
34 |
'attribute' => 'start_time', |
35 |
'template' => '{input}{button}', |
36 |
//'language' => 'en',
|
37 |
'size' => 'ms', |
38 |
'clientOptions' => [ |
39 |
'autoclose' => true, |
40 |
'format' => 'H:ii p', |
41 |
'todayBtn' => false, |
42 |
'minuteStep'=> 15, |
43 |
'showMeridian'=>true, |
44 |
//'pickerPosition' => 'bottom-left',
|
45 |
'startView'=>1, |
46 |
'minView'=>0, |
47 |
'maxView'=>1, |
48 |
// to do - format one day ahead
|
49 |
//'initialDate'=> Date('Y-m-d'),
|
50 |
// $( "th.switch" ).text( "Pick the time" );
|
51 |
]
|
52 |
]);?> |
53 |
</div>
|
54 |
<p></p>
|
55 |
</div>
|
56 |
<div class="col-xs-6 col-md-2 col-lg-2"> |
57 |
<?php
|
58 |
$durationList = [1=>'1 hour',2=>'2 hours',3=>'3 hours',4=>'4 hours',5=>'5 hours',6=>'6 hours',12=>'12 hours',24=>'24 hours',48=>'48 hours',72=>'72 hours']; |
59 |
echo $form->field($model, 'duration',['options' => ['id'=>'duration','class' => 'duration-width' ]]) |
60 |
->dropDownList( |
61 |
$durationList, // Flat array ('id'=>'label') |
62 |
['prompt'=>'select a duration'] // options |
63 |
);
|
64 |
?>
|
65 |
</div>
|
66 |
<div class="col-xs-6 col-md-2 col-lg-2" style="margin-top:3em;"> |
67 |
<?= Html::a(Yii::t('frontend','advanced options'),'javascript:void(0);', ['onclick'=>'toggleTimeAdvanced();']);?> |
68 |
</div>
|
69 |
</div>
|
By using successful column dimensions in Bootstrap like this, the row spreads out on desktop (shown below) and collapses on itself into three rows on mobile (shown above):
1 |
<div class="col-xs-12 col-md-4 col-lg-3"> |
2 |
<!-- Date -->
|
3 |
... |
4 |
<div class="col-xs-12 col-md-4 col-lg-3"> |
5 |
<!-- Time -->
|
6 |
... |
7 |
<div class="col-xs-6 col-md-2 col-lg-2"> |
8 |
<!-- Duration -->
|
9 |
... |
10 |
<div class="col-xs-6 col-md-2 col-lg-2" style="margin-top:3em;"> |
11 |
<!-- Advanced options -->
|
12 |
... |



The jQuery toggleTimeAdvanced()
for the advanced options link opens the repetition form by removing the hidden
class:
1 |
function toggleTimeAdvanced() { |
2 |
if ($('#timeAdvanced').hasClass('hidden')) { |
3 |
$('#timeAdvanced').removeClass('hidden'); |
4 |
} else { |
5 |
$('#timeAdvanced').addClass('hidden'); |
6 |
$("select#meetingtime-repeat_quantity").prop('selectedIndex', 0); |
7 |
}
|
Note: All the jQuery can be found in /frontend/web/js/meeting.js.
It also resets the repetition setting to zero when you close it—that was a design decision to prevent duplicates from being created if people closed the advanced form.
Here's the timeAdvanced
sub-form:
1 |
<div class="row hidden" id="timeAdvanced"> |
2 |
<div class="col-xs-12 col-md-2 col-lg-2"> |
3 |
<?php
|
4 |
$repeat_quantity = [0=>'no repeating',1=>'1 additional option', |
5 |
2=>'2 additional options',3=>'3 additional options', |
6 |
4=>'4 additional options',5=>'5 additional options']; |
7 |
echo $form->field($model, 'repeat_quantity',['options' => ['id'=>'repeat_quantity','class' => 'repeat-width' ]])->label('Add') |
8 |
->dropDownList( |
9 |
$repeat_quantity
|
10 |
,
|
11 |
['options'=>['1'=>['Selected'=>true]]] |
12 |
);
|
13 |
?>
|
14 |
</div>
|
15 |
<div class="col-xs-12 col-md-6 col-lg-6"> |
16 |
<?php
|
17 |
$repeat_unit = ['hour'=>'successive hour e.g. 9 am, 10 am and 11 am', |
18 |
'day'=>'successive day e.g. Monday, Tuesday & Wednesday', |
19 |
'week'=>'successive week e.g. next Friday & Friday after']; |
20 |
echo $form->field($model, 'repeat_unit',['options' => ['id'=>'repeat_unit','class' => 'repeat-width' ]])->label('On each') |
21 |
->dropDownList( |
22 |
$repeat_unit
|
23 |
);
|
24 |
?>
|
25 |
</div>
|
26 |
</div>
|
The Bootstrap I used appears in one row on desktops and two rows on mobile devices:
1 |
<div class="col-xs-12 col-md-2 col-lg-2"> |
2 |
<!-- repeat quantity -->
|
3 |
<div class="col-xs-12 col-md-6 col-lg-6"> |
4 |
<!-- repeat unit -->
|
Here's what it looks like adding 3 additional options each successive day at 9 am:



Next, I updated the addTime()
function to capture and submit the repeat_quantity
and repeat_unit
fields to the PHP-based controller:
1 |
function addTime(id) { |
2 |
start_time = $('#meetingtime-start_time').val(); |
3 |
start = $('#meetingtime-start').val(); |
4 |
duration = $('#meetingtime-duration').val(); |
5 |
repeat_quantity = $('#meetingtime-repeat_quantity').val(); |
6 |
repeat_unit = $('#meetingtime-repeat_unit').val(); |
7 |
if (start_time =='' || start=='') { |
8 |
displayAlert('timeMessage','timeMsg2'); |
9 |
return false; |
10 |
}
|
11 |
// ajax submit subject and message
|
12 |
$.ajax({ |
13 |
url: $('#url_prefix').val()+'/meeting-time/add', |
14 |
data: { |
15 |
id: id, |
16 |
start_time: encodeURIComponent(start_time), |
17 |
start:encodeURIComponent(start), |
18 |
duration:encodeURIComponent(duration), |
19 |
repeat_quantity:encodeURIComponent(repeat_quantity), |
20 |
repeat_unit:encodeURIComponent(repeat_unit), |
21 |
},
|
22 |
success: function(data) { |
23 |
loadTimeChoices(id); |
24 |
insertTime(id); |
25 |
displayAlert('timeMessage','timeMsg1'); |
26 |
return true; |
27 |
}
|
28 |
});
|
Startups are hard in that you're always rushing to get new features done. For example, someone (likely me since I'm the only coder) had never transferred the chosen duration; so, I added that too. Up until today, all the meetings were 1 hour despite what users requested. Enough said. #startuplife.
Then, I switched over to the MVC code in my Yii Framework-based /frontend/controllers/MeetingTimeController.php. Below, you can see the actionAdd
AJAX method that responds to the jQuery submission:
1 |
public function actionAdd($id,$start,$start_time,$duration=1,$repeat_quantity=0,$repeat_unit='hour') { |
2 |
Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; |
3 |
$timezone = MiscHelpers::fetchUserTimezone(Yii::$app->user->getId()); |
4 |
date_default_timezone_set($timezone); |
5 |
$cnt=0; |
6 |
while ($cnt<=$repeat_quantity) { |
7 |
$model = new MeetingTime(); |
8 |
$model->start = urldecode($start); |
9 |
$model->start_time = urldecode($start_time); |
10 |
if (empty($model->start)) { |
11 |
$model->start = Date('M d, Y',time()+3*24*3600); |
12 |
}
|
13 |
$model->tz_current = $timezone; |
14 |
$model->duration = $duration; |
15 |
$model->meeting_id= $id; |
16 |
$model->suggested_by= Yii::$app->user->getId(); |
17 |
$model->status = MeetingTime::STATUS_SUGGESTED; |
18 |
$selected_time = date_parse($model->start_time); |
19 |
if ($selected_time['hour'] === false) { |
20 |
$selected_time['hour'] =9; |
21 |
$selected_time['minute'] =0; |
22 |
}
|
23 |
// convert date time to timestamp
|
24 |
$model->start = strtotime($model->start) + $selected_time['hour']*3600+ $selected_time['minute']*60; |
25 |
if ($cnt>0) { |
26 |
switch ($repeat_unit) { |
27 |
case 'hour': |
28 |
$model->start+=($cnt*3600); |
29 |
break; |
30 |
case 'day': |
31 |
$model->start+=($cnt*24*3600); |
32 |
break; |
33 |
case 'week': |
34 |
$model->start+=($cnt*7*24*3600); |
35 |
break; |
36 |
}
|
37 |
}
|
38 |
$model->end = $model->start + (3600*$model->duration); |
39 |
$model->save(); |
40 |
$cnt+=1; |
41 |
}
|
42 |
return true; |
43 |
}
|
Basically, I created a loop using a counter, $cnt
, to increment the MeetingTime start and end time choices by the $repeat_unit
, e.g. hours, days, or weeks:
1 |
if ($cnt>0) { |
2 |
switch ($repeat_unit) { |
3 |
case 'hour': |
4 |
$model->start+=($cnt*3600); |
5 |
break; |
6 |
case 'day': |
7 |
$model->start+=($cnt*24*3600); |
8 |
break; |
9 |
case 'week': |
10 |
$model->start+=($cnt*7*24*3600); |
11 |
break; |
12 |
}
|
13 |
}
|
14 |
$model->end = $model->start + (3600*$model->duration); |
So here are the results of me adding three additional timeslots each day at 9:00 AM:



So now, it's easier to schedule meetings with people and offer them several successive dates and times as options for getting together.
In Closing
I hope this has been helpful to you seeing how Bootstrap can be used to create better forms and can be combined with Ajax and jQuery to build a simple interactive experience for your users.
If you didn't earlier, try scheduling a meeting at Meeting Planner with repeating date/time options and let me know what you think.
Have your own thoughts? Ideas? Feedback? You can always reach me on Twitter @lookahead_io directly. Watch for upcoming tutorials here in the Building Your Startup With PHP series.
Over the next few weeks, I'm going to continue polishing the user experience to make the service as easy as possible to use. For example, you might notice the meeting notes are now on their own tab:



And, to eliminate the confusion people were having between the availability column of yes/no switches and the second column of choosing the final place, I separated this into a lower sub-panel of buttons, Finalize the Time. Only organizers and participants designated as organizers see this lower panel, simplifying the common view for typical participants:



Bootstrap, jQuery and Ajax tied partly or wholly into building both of these features as well.
I hope by now in the series, you're having your own startup ideas and thinking about writing some code. Stay tuned to learn more about how I'm building and launching mine.