Multiple Modals
This intermediate guide shows how to create a multiple modal form.
The final widget looks like:
See the Pen CanJS 6.0 - Multiple Modals - Final by Bitovi (@bitovi) on CodePen.
The following sections are broken down the following parts:
The problem - A description of what the section is trying to accomplish.
What you need to know - Information about CanJS that is useful for solving the problem.
How to verify it works - How to make sure the solution works if it’s not obvious.
The solution - The solution to the problem.
Setup
The problem
In this section, we will fork this CodePen that contains some starting code that we will modify to use modals instead of adding each form directly in the page.
What you need to know
The CodePen creates and several basic components:
<occupation-questions>
- A form that asks the sorts of things the user does.<diva-questions>
- A form that asks for expenses for divas.<programmer-questions>
- A form that asks for a programmer's programming language.<income-questions>
- A form that asks how the user gets paid.<my-app>
- The main application component. It uses all of the above components to update its value.
<my-app>
is mounted in the HTML
tab as follows:
<my-app></my-app>
The solution
START THIS TUTORIAL BY CLONING THE FOLLOWING CODEPEN:
Click the
EDIT ON CODEPEN
button. The CodePen will open in a new window. In that new window, clickFORK
.
See the Pen CanJS 6.0 - Multiple Modals - Setup by Bitovi (@bitovi) on CodePen.
This CodePen:
- Loads all of CanJS’s packages. Each package is available as a named export. For example can-stache-element
is available as
import { StacheElement } from "can"
.
Create a simple modal
The problem
In this section, we will:
- Create a simple
<my-modal>
custom element that will put its "light DOM" within a modal window. - Show the
<diva-questions>
component whenisDiva
is set to true.
What you need to know
Use
{{# if(value) }} HTML {{/ if }}
to showHTML
whenvalue
is true.Content between custom element tags like:
<custom-element>SOME CONTENT</custom-element>
Is available to be rendered with the
<content>
element within the custom element’sview
. The following would putSOME CONTENT
within an<h1>
element:class CustomElement extends StacheElement { static view = `<h1><content></content></h1>`; } customElements.define("custom-element", CustomElement);
How to verify it works
If you click the isDiva
radio input, a modal window with the <diva-questions>
form should appear.
The solution
Update the JS
tab to:
import {
stache,
stacheConverters,
StacheElement,
type,
ObservableArray
} from "//unpkg.com/can@6/ecosystem.mjs";
stache.addConverter(stacheConverters);
class OccupationQuestions extends StacheElement {
static view = `
<h3>Occupation</h3>
<div class="content">
<p>
Are you a diva?
<input id="diva-yes" type="radio" checked:bind="equal(this.isDiva, true)">
<label for="diva-yes">yes</label>
<input id="diva-no" type="radio" checked:bind="equal(this.isDiva, false)">
<label for="diva-no">no</label>
</p>
<p>
Do you program?
<input
id="programmer-yes"
type="radio"
checked:bind="equal(this.isProgrammer, true)"
>
<label for="programmer-yes">yes</label>
<input
id="programmer-no"
type="radio"
checked:bind="equal(this.isProgrammer, false)"
>
<label for="programmer-no">no</label>
</p>
<p><button on:click="this.next()">Next</button></p>
</div>
`;
static props = {
isDiva: Boolean,
isProgrammer: Boolean
};
}
customElements.define("occupation-questions", OccupationQuestions);
class DivaQuestions extends StacheElement {
static view = `
<h3>Diva Questions</h3>
<div class="content">
<p>Check all expenses that apply:</p>
<p>
<input
id="swagger"
type="checkbox"
checked:bind="boolean-to-inList('Swagger', this.divaExpenses)"
>
<label for="swagger">Swagger</label>
</p>
<p>
<input
id="fame"
type="checkbox"
checked:bind="boolean-to-inList('Fame', this.divaExpenses)"
>
<label for="fame">Fame</label>
</p>
<p><button on:click="this.next()">Next</button></p>
</div>
`;
static props = {
divaExpenses: type.Any
};
}
customElements.define("diva-questions", DivaQuestions);
class ProgrammerQuestions extends StacheElement {
static view = `
<h3>Programmer Questions</h3>
<div class="content">
<p>What is your favorite language?</p>
<p>
<select value:to="this.programmingLanguage">
<option>C</option>
<option>C++</option>
<option>Java</option>
<option>JavaScript</option>
</select>
</p>
<p><button on:click="this.next()">Next</button></p>
</div>
`;
static props = {
programmingLanguage: String
};
}
customElements.define("programmer-questions", ProgrammerQuestions);
class IncomeQuestions extends StacheElement {
static view = `
<h3>Income</h3>
<div class="content">
<p>What do you get paid in?</p>
<p>
<select value:bind="string-to-any(this.paymentType)">
<option value="undefined">Select a type</option>
<option>Peanuts</option>
<option>Bread</option>
<option>Tamales</option>
<option>Cheddar</option>
<option>Dough</option>
</select>
</p>
<p><button on:click="this.next()">Finish</button></p>
</div>
`;
static props = {
paymentType: type.maybeConvert(String)
};
}
customElements.define("income-questions", IncomeQuestions);
class MyModals extends StacheElement {
static view = `
<div class="background"></div>
<div class="modal-container">
<content>Supply some content</content>
</div>
`;
}
customElements.define("my-modals", MyModals);
class MyApp extends StacheElement {
static view = `
<occupation-questions
isDiva:bind="this.isDiva"
isProgrammer:bind="this.isProgrammer"
/>
<p>isDiva: {{ this.isDiva }}</p>
<p>isProgrammer: {{ this.isProgrammer }}</p>
{{# if(this.isDiva) }}
<my-modals>
<diva-questions divaExpenses:bind="this.divaExpenses" />
</my-modals>
{{/ if }}
<p>diva expenses: {{ this.divaExpenses.join(', ') }}</p>
<programmer-questions programmingLanguage:bind="this.programmingLanguage" />
<p>programmingLanguage: {{ this.programmingLanguage }}</p>
<income-questions paymentType:bind="this.paymentType" />
<p>paymentType: {{ this.paymentType }}</p>
`;
static props = {
// Stateful properties
isDiva: false,
divaExpenses: {
get default() {
return new ObservableArray();
}
},
isProgrammer: false,
programmingLanguage: String,
paymentType: String
// Derived properties
};
// Methods
}
customElements.define("my-app", MyApp);
Pass a component instance
The problem
In this section, we are matching the same behavior as the
previous example. However, we are going to change the <my-modals>
component to take a component instance to render
in a modal instead of "light DOM".
What you need to know
Use
{ get default() { /* ... */ }}
to create a default value for a property:class MyComponent extends StacheElement { static props = { dueDate: { get default(){ return new Date(); } } }; }
Component instances can be created like:
const component = new ProgrammerQuestions().initialize({ programmingLanguage: "JS" });
This is roughly equivalent to:
<programmer-questions programmingLanguage:from="'JS'" />
Use can-value and bindings to setup a two-way binding from one component to another:
static props = { get default() { return new ProgrammerQuestions().bindings({ programmingLanguage: value.bind(this, "programmingLanguage") }); } };
This is roughly equivalent to:
<programmer-questions programmingLanguage:bind="this.programmingLanguage" />
Render a component instance with
{{ component }}
.
The solution
Update the JS
tab to:
import {
stache,
stacheConverters,
StacheElement,
type,
ObservableArray
} from "//unpkg.com/can@6/ecosystem.mjs";
stache.addConverter(stacheConverters);
class OccupationQuestions extends StacheElement {
static view = `
<h3>Occupation</h3>
<div class="content">
<p>
Are you a diva?
<input id="diva-yes" type="radio" checked:bind="equal(this.isDiva, true)">
<label for="diva-yes">yes</label>
<input id="diva-no" type="radio" checked:bind="equal(this.isDiva, false)">
<label for="diva-no">no</label>
</p>
<p>
Do you program?
<input
id="programmer-yes"
type="radio"
checked:bind="equal(this.isProgrammer, true)"
>
<label for="programmer-yes">yes</label>
<input
id="programmer-no"
type="radio"
checked:bind="equal(this.isProgrammer, false)"
>
<label for="programmer-no">no</label>
</p>
<p><button on:click="this.next()">Next</button></p>
</div>
`;
static props = {
isDiva: Boolean,
isProgrammer: Boolean
};
}
customElements.define("occupation-questions", OccupationQuestions);
class DivaQuestions extends StacheElement {
static view = `
<h3>Diva Questions</h3>
<div class="content">
<p>Check all expenses that apply:</p>
<p>
<input
id="swagger"
type="checkbox"
checked:bind="boolean-to-inList('Swagger', this.divaExpenses)"
>
<label for="swagger">Swagger</label>
</p>
<p>
<input
id="fame"
type="checkbox"
checked:bind="boolean-to-inList('Fame', this.divaExpenses)"
>
<label for="fame">Fame</label>
</p>
<p><button on:click="this.next()">Next</button></p>
</div>
`;
static props = {
divaExpenses: type.Any
};
}
customElements.define("diva-questions", DivaQuestions);
class ProgrammerQuestions extends StacheElement {
static view = `
<h3>Programmer Questions</h3>
<div class="content">
<p>What is your favorite language?</p>
<p>
<select value:to="this.programmingLanguage">
<option>C</option>
<option>C++</option>
<option>Java</option>
<option>JavaScript</option>
</select>
</p>
<p><button on:click="this.next()">Next</button></p>
</div>
`;
static props = {
programmingLanguage: String
};
}
customElements.define("programmer-questions", ProgrammerQuestions);
class IncomeQuestions extends StacheElement {
static view = `
<h3>Income</h3>
<div class="content">
<p>What do you get paid in?</p>
<p>
<select value:bind="string-to-any(this.paymentType)">
<option value="undefined">Select a type</option>
<option>Peanuts</option>
<option>Bread</option>
<option>Tamales</option>
<option>Cheddar</option>
<option>Dough</option>
</select>
</p>
<p><button on:click="this.next()">Finish</button></p>
</div>
`;
static props = {
paymentType: type.maybeConvert(String)
};
}
customElements.define("income-questions", IncomeQuestions);
class MyModals extends StacheElement {
static view = `
<div class="background"></div>
<div class="modal-container">
{{ this.component }}
</div>
`;
}
customElements.define("my-modals", MyModals);
class MyApp extends StacheElement {
static view = `
<occupation-questions
isDiva:bind="this.isDiva"
isProgrammer:bind="this.isProgrammer"
/>
<p>isDiva: {{ this.isDiva }}</p>
<p>isProgrammer: {{ this.isProgrammer }}</p>
{{# if(this.isDiva) }}
<my-modals component:from="this.divaQuestions"></my-modals>
{{/ if }}
<p>diva expenses: {{ this.divaExpenses.join(', ') }}</p>
<programmer-questions programmingLanguage:bind="this.programmingLanguage" />
<p>programmingLanguage: {{ this.programmingLanguage }}</p>
<income-questions paymentType:bind="this.paymentType" />
<p>paymentType: {{ this.paymentType }}</p>
`;
static props = {
// Stateful properties
isDiva: false,
divaExpenses: {
get default() {
return new ObservableArray();
}
},
isProgrammer: false,
programmingLanguage: String,
paymentType: String,
divaQuestions: {
get default() {
return new DivaQuestions().bindings({
divaExpenses: value.bind(this, "divaExpenses")
});
}
},
// Derived properties
};
// Methods
}
customElements.define("my-app", MyApp);
Show multiple modals in the window
The problem
In this section, we will:
- Show all form components within a modal box.
- Show the
<diva-questions>
and<programmer-questions>
modals only if their respective questions (isDiva
andisProgrammer
) checkboxes are selected. - Remove all the form components from being rendered in the main page content area.
We will do this by:
- Changing
<my-modals>
to:- take an array of component instances.
- position the component instances within
<div class="modal-container">
elements 20 pixels apart.
- Changing
<my-app>
to:- create instances for the
OccupationQuestions
,ProgrammerQuestions
, andIncomeQuestions
components. - create a
visibleQuestions
array that contains only the instances that should be presented to the user.
- create instances for the
What you need to know
Use ES5 getters to transform component's stateful properties to new values. For example, the following returns
true
if someone is a diva and a programmer:static props = { isDiva: Boolean, isProgrammer: Boolean, get isDivaAndProgrammer() { return this.isDiva && this.isProgrammer; } };
This can be used to derive the
visibleQuestions
array.
The solution
Update the JS
tab to:
import {
stache,
stacheConverters,
StacheElement,
type,
ObservableArray
} from "//unpkg.com/can@6/ecosystem.mjs";
stache.addConverter(stacheConverters);
class OccupationQuestions extends StacheElement {
static view = `
<h3>Occupation</h3>
<div class="content">
<p>
Are you a diva?
<input id="diva-yes" type="radio" checked:bind="equal(this.isDiva, true)">
<label for="diva-yes">yes</label>
<input id="diva-no" type="radio" checked:bind="equal(this.isDiva, false)">
<label for="diva-no">no</label>
</p>
<p>
Do you program?
<input
id="programmer-yes"
type="radio"
checked:bind="equal(this.isProgrammer, true)"
>
<label for="programmer-yes">yes</label>
<input
id="programmer-no"
type="radio"
checked:bind="equal(this.isProgrammer, false)"
>
<label for="programmer-no">no</label>
</p>
<p><button on:click="this.next()">Next</button></p>
</div>
`;
static props = {
isDiva: Boolean,
isProgrammer: Boolean
};
}
customElements.define("occupation-questions", OccupationQuestions);
class DivaQuestions extends StacheElement {
static view = `
<h3>Diva Questions</h3>
<div class="content">
<p>Check all expenses that apply:</p>
<p>
<input
id="swagger"
type="checkbox"
checked:bind="boolean-to-inList('Swagger', this.divaExpenses)"
>
<label for="swagger">Swagger</label>
</p>
<p>
<input
id="fame"
type="checkbox"
checked:bind="boolean-to-inList('Fame', this.divaExpenses)"
>
<label for="fame">Fame</label>
</p>
<p><button on:click="this.next()">Next</button></p>
</div>
`;
static props = {
divaExpenses: type.Any
};
}
customElements.define("diva-questions", DivaQuestions);
class ProgrammerQuestions extends StacheElement {
static view = `
<h3>Programmer Questions</h3>
<div class="content">
<p>What is your favorite language?</p>
<p>
<select value:to="this.programmingLanguage">
<option>C</option>
<option>C++</option>
<option>Java</option>
<option>JavaScript</option>
</select>
</p>
<p><button on:click="this.next()">Next</button></p>
</div>
`;
static props = {
programmingLanguage: String
};
}
customElements.define("programmer-questions", ProgrammerQuestions);
class IncomeQuestions extends StacheElement {
static view = `
<h3>Income</h3>
<div class="content">
<p>What do you get paid in?</p>
<p>
<select value:bind="string-to-any(this.paymentType)">
<option value="undefined">Select a type</option>
<option>Peanuts</option>
<option>Bread</option>
<option>Tamales</option>
<option>Cheddar</option>
<option>Dough</option>
</select>
</p>
<p><button on:click="this.next()">Finish</button></p>
</div>
`;
static props = {
paymentType: type.maybeConvert(String)
};
}
customElements.define("income-questions", IncomeQuestions);
class MyModals extends StacheElement {
static view = `
{{# for(componentData of this.componentsToShow) }}
{{# if(componentData.last) }}
<div class="background"></div>
{{/ if }}
<div
class="modal-container"
style="margin-top: {{ this.componentData.position }}px; margin-left: {{ this.componentData.position }}px"
>
{{ componentData.component }}
</div>
{{/ for }}
`;
static props = {
get componentsToShow() {
let distance = 20;
let count = this.components.length;
let start = -150 - distance / 2 * (count - 1);
return this.components.map(function(component, i) {
return {
position: start + i * distance,
component: component,
last: i === count - 1
};
});
}
};
}
customElements.define("my-modals", MyModals);
class MyApp extends StacheElement {
static view = `
<my-modals components:from="this.visibleQuestions"></my-modals>
<p>isDiva: {{ this.isDiva }}</p>
<p>isProgrammer: {{ this.isProgrammer }}</p>
<p>diva expenses: {{ this.divaExpenses.join(', ') }}</p>
<p>programmingLanguage: {{ this.programmingLanguage }}</p>
<p>paymentType: {{ this.paymentType }}</p>
`;
static props = {
// Stateful properties
isDiva: false,
divaExpenses: {
get default() {
return new ObservableArray();
}
},
isProgrammer: false,
programmingLanguage: String,
paymentType: String,
occupationQuestions: {
get default() {
return new OccupationQuestions().bindings({
isDiva: value.bind(this, "isDiva"),
isProgrammer: value.bind(this, "isProgrammer")
});
}
},
divaQuestions: {
get default() {
return new DivaQuestions().bindings({
divaExpenses: value.bind(this, "divaExpenses")
});
}
},
programmerQuestions: {
get default() {
return new ProgrammerQuestions().bindings({
programmingLanguage: value.bind(this, "programmingLanguage")
});
}
},
incomeQuestions: {
get default() {
return new IncomeQuestions().bindings({
paymentType: value.bind(this, "paymentType")
});
}
},
// Derived properties
get allQuestions() {
var forms = [this.occupationQuestions];
if (this.isDiva) {
forms.push(this.divaQuestions);
}
if (this.isProgrammer) {
forms.push(this.programmerQuestions);
}
forms.push(this.incomeQuestions);
return new ObservableArray(forms);
},
get visibleQuestions() {
return this.allQuestions.slice(0).reverse();
}
};
// Methods
}
customElements.define("my-app", MyApp);
Next should move to the next window
The problem
In this section, we will make it so when someone clicks the Next
button in a modal, the next modal window will be displayed.
What you need to know
We can use a index of which question we have answered to know which
questions should be returned by visibleQuestions
.
The following creates a counting index and a method that increments it:
class MyModals extends StacheElement {
// ...
static props = {
questionIndex: { default: 0 }
};
next() {
this.questionIndex += 1;
}
}
To pass the next
function to a component, you must make sure that the right this
is preserved. You can do that with function.bind
like:
new ProgrammerQuestions().bindings({
programmingLanguage: value.bind(this, "programmingLanguage"),
next: this.next.bind(this)
});
The solution
import {
ObservableArray,
stache,
stacheConverters,
StacheElement,
type,
value
} from "//unpkg.com/can@pre/ecosystem.mjs";
stache.addConverter(stacheConverters);
class OccupationQuestions extends StacheElement {
static view = `
<h3>Occupation</h3>
<div class="content">
<p>
Are you a diva?
<input id="diva-yes" type="radio" checked:bind="equal(this.isDiva, true)">
<label for="diva-yes">yes</label>
<input id="diva-no" type="radio" checked:bind="equal(this.isDiva, false)">
<label for="diva-no">no</label>
</p>
<p>
Do you program?
<input
id="programmer-yes"
type="radio"
checked:bind="equal(this.isProgrammer, true)"
>
<label for="programmer-yes">yes</label>
<input
id="programmer-no"
type="radio"
checked:bind="equal(this.isProgrammer, false)"
>
<label for="programmer-no">no</label>
</p>
<p><button on:click="this.next()">Next</button></p>
</div>
`;
static props = {
isDiva: Boolean,
isProgrammer: Boolean
};
}
customElements.define("occupation-questions", OccupationQuestions);
class DivaQuestions extends StacheElement {
static view = `
<h3>Diva Questions</h3>
<div class="content">
<p>Check all expenses that apply:</p>
<p>
<input
id="swagger"
type="checkbox"
checked:bind="boolean-to-inList('Swagger', this.divaExpenses)"
>
<label for="swagger">Swagger</label>
</p>
<p>
<input
id="fame"
type="checkbox"
checked:bind="boolean-to-inList('Fame', this.divaExpenses)"
>
<label for="fame">Fame</label>
</p>
<p><button on:click="this.next()">Next</button></p>
</div>
`;
static props = {
divaExpenses: type.Any
};
}
customElements.define("diva-questions", DivaQuestions);
class ProgrammerQuestions extends StacheElement {
static view = `
<h3>Programmer Questions</h3>
<div class="content">
<p>What is your favorite language?</p>
<p>
<select value:to="this.programmingLanguage">
<option>C</option>
<option>C++</option>
<option>Java</option>
<option>JavaScript</option>
</select>
</p>
<p><button on:click="this.next()">Next</button></p>
</div>
`;
static props = {
programmingLanguage: String
};
}
customElements.define("programmer-questions", ProgrammerQuestions);
class IncomeQuestions extends StacheElement {
static view = `
<h3>Income</h3>
<div class="content">
<p>What do you get paid in?</p>
<p>
<select value:bind="string-to-any(this.paymentType)">
<option value="undefined">Select a type</option>
<option>Peanuts</option>
<option>Bread</option>
<option>Tamales</option>
<option>Cheddar</option>
<option>Dough</option>
</select>
</p>
<p><button on:click="this.next()">Finish</button></p>
</div>
`;
static props = {
paymentType: type.maybeConvert(String)
};
}
customElements.define("income-questions", IncomeQuestions);
class MyModals extends StacheElement {
static view = `
{{# for(component of componentsToShow) }}
{{# if(component.last) }}
<div class="background"></div>
{{/ if }}
<div
class="modal-container"
style="margin-top: {{ component.position }}px; margin-left: {{ component.position }}px"
>
{{ component.content }}
</div>
{{/ for }}
`;
static props = {
get componentsToShow() {
const distance = 20;
const count = this.components.length;
const start = -150 - distance / 2 * (count - 1);
return this.components.map(function(component, i) {
return {
position: start + i * distance,
content: component,
last: i === count - 1
};
});
}
};
}
customElements.define("my-modals", MyModals);
class MyApp extends StacheElement {
static view = `
<my-modals components:from="this.visibleQuestions"></my-modals>
<p>isDiva: {{ this.isDiva }}</p>
<p>isProgrammer: {{ this.isProgrammer }}</p>
<p>diva expenses: {{ this.divaExpenses.join(', ') }}</p>
<p>programmingLanguage: {{ this.programmingLanguage }}</p>
<p>paymentType: {{ this.paymentType }}</p>
`;
static props = {
// Stateful properties
isDiva: { type: Boolean, default: false },
divaExpenses: {
get default() {
return new ObservableArray();
}
},
isProgrammer: { type: Boolean, default: false },
programmingLanguage: type.maybeConvert(String),
paymentType: String,
occupationQuestions: {
get default() {
return new OccupationQuestions().bindings({
isDiva: value.bind(this, "isDiva"),
isProgrammer: value.bind(this, "isProgrammer"),
next: this.next.bind(this)
});
}
},
divaQuestions: {
get default() {
return new DivaQuestions().bindings({
divaExpenses: value.bind(this, "divaExpenses"),
next: this.next.bind(this)
});
}
},
programmerQuestions: {
get default() {
return new ProgrammerQuestions().bindings({
programmingLanguage: value.bind(this, "programmingLanguage"),
next: this.next.bind(this)
});
}
},
incomeQuestions: {
get default() {
return new IncomeQuestions().bindings({
paymentType: value.bind(this, "paymentType"),
next: this.next.bind(this)
});
}
},
questionIndex: { default: 0 },
// Derived properties
get allQuestions() {
var forms = [this.occupationQuestions];
if (this.isDiva) {
forms.push(this.divaQuestions);
}
if (this.isProgrammer) {
forms.push(this.programmerQuestions);
}
forms.push(this.incomeQuestions);
return new ObservableArray(forms);
},
get visibleQuestions() {
return this.allQuestions.slice(this.questionIndex).reverse();
}
};
// Methods
next() {
this.questionIndex += 1;
}
}
customElements.define("my-app", MyApp);
Result
When complete, you should have a working multiple modal form like the following CodePen:
See the Pen CanJS 6.0 - Multiple Modals - Final by Bitovi (@bitovi) on CodePen.