Skip to content

Instantly share code, notes, and snippets.

@freshcutdevelopment
Last active November 21, 2018 18:46
Show Gist options
  • Save freshcutdevelopment/c9c19fbe1e147fc39ac864cf0b694fd1 to your computer and use it in GitHub Desktop.
Save freshcutdevelopment/c9c19fbe1e147fc39ac864cf0b694fd1 to your computer and use it in GitHub Desktop.
Aurelia Gist - Add book form
<template>
<require from="book-form"></require>
<book-form></book-form>
<div class="notification" show.bind="notification.length > 0">
${notification}
</div>
</template>
import {EventAggregator} from 'aurelia-event-aggregator';
import {inject} from 'aurelia-framework';
@inject(EventAggregator)
export class App {
constructor(eventAggregator){
this.eventAggregator = eventAggregator;
}
bind(){
this.eventAggregator.subscribe('book-added', _ => {
this.notification = "Added new book";
setTimeout(_ => this.notification = "", 1000);
});
}
unbind(){
this.eventAggregator.dispose();
}
}
<template>
<require from="typeahead"></require>
<require from="star-rating"></require>
<form class="form card" submit.trigger="addBook()">
<div class="card-block">
<h4 class="card-title">Add book <i class="fa fa-book" aria-hidden="true"></i></h4>
<h6 class="card-subtitle mb-2 text-muted">add a book to your bookshelf</h6>
<hr/>
<div class="form-group">
<label for="title">Title</label>
<input name="title" class="form-control"
placeholder='enter a title'
value.bind="title & validate"></input>
</div>
<div class="form-group">
<label for="genre">Genre</label>
<input value.bind="genre"
typeahead="items.bind:genres"
type="text"
name="genre"
class="form-control"
ref="selectGenreElement"></input>
</div>
<div class="form-group">
<label for="times-read">Times read</label>
<input name=times-read class="form-control" value.bind="timesRead & validate"></input>
</div>
<hr/>
<star-rating
view-model.ref="starRatingViewModel"
ref="ratingElement"
rating.bind="rating">
</star-rating>
</form>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary col-sm-3 push-sm-9"
disabled.bind="title.length == 0">
add
</button>
</div>
</div>
</template>
import {inject,NewInstance} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import {BooksApi} from 'books-api';
import {BootstrapFormRenderer} from 'bootstrap-form-renderer';
import {ValidationRules, ValidationController} from 'aurelia-validation';
@inject(EventAggregator, BooksApi, NewInstance.of(ValidationController))
export class BookForm{
constructor(eventAggregator, bookApi, controller){
this.title = "";
this.eventAggregator = eventAggregator;
this.bookApi = bookApi;
this.controller = controller;
this.controller.addRenderer(new BootstrapFormRenderer());
this.configureValidationRules();
this.createEventListeners();
}
configureValidationRules(){
ValidationRules.customRule(
'positiveInteger',
(value, obj) => value === null || value === undefined
|| (Number.isInteger(value) || value >= 0),
`Books can only be read 0 or more times.`
);
ValidationRules
.ensure('title').required()
.ensure('timesRead')
.required()
.satisfiesRule('positiveInteger').
on(this);
}
addBook(){
this.controller.validate().then(result => {
if(result.valid) this.eventAggregator.publish('book-added');
});
}
bind(){
this.bookApi.getGenres().then(genres => {
this.genres = genres;
});
}
createEventListeners(){
this.genreSelectedListener = e => {
if(e && e.detail){
this.genre = e.detail.value;
}
};
this.ratingChangedListener = e => this.rating = e.rating;
}
attached(){
this.selectGenreElement.addEventListener("change", this.genreSelectedListener );
this.selectGenreElement.addEventListener("change", this.ratingChangedListener );
}
detached(){
this.ratingElement.removeEventListener('change', this.ratingChangedListener);
this.selectGenreElement.removeEventListener('change', this.genreSelectedListener);
}
}
export class BooksApi{
constructor(){
this.simulatedLatency = 500;
}
getGenres(){
let genres = [
'Art',
'Autobiographies',
'Drama',
'Childrens',
'Fantasy',
'History',
'Mystery',
'Romance',
'Science',
'Science Fiction'
];
return this.simulateFetch(genres);
}
simulateFetch(fetchResult){
return new Promise(resolve => {
setTimeout(() => {
resolve(fetchResult);
}, this.simulatedLatency);
});
}
}
import {
ValidationRenderer,
RenderInstruction,
ValidateResult
} from 'aurelia-validation';
export class BootstrapFormRenderer {
render(instruction) {
for (let { result, elements } of instruction.unrender) {
for (let element of elements) {
this.remove(element, result);
}
}
for (let { result, elements } of instruction.render) {
for (let element of elements) {
this.add(element, result);
}
}
}
add(element, result) {
if (result.valid) {
return;
}
const formGroup = element.closest('.form-group');
if (!formGroup) {
return;
}
// add the has-error class to the enclosing form-group div
formGroup.classList.add('has-danger');
// add help-block
const message = document.createElement('div');
message.className = 'form-control-feedback mb-2 mr-sm-2 mb-sm-0';
message.textContent = result.message;
message.id = `validation-message-${result.id}`;
element.classList.add('form-control-danger');
formGroup.appendChild(message);
}
remove(element, result) {
if (result.valid) {
return;
}
const formGroup = element.closest('.form-group');
if (!formGroup) {
return;
}
// remove help-block
const message = formGroup.querySelector(`#validation-message-${result.id}`);
if (message) {
formGroup.removeChild(message);
// remove the has-error class from the enclosing form-group div
if (formGroup.querySelectorAll('.help-block.validation-message').length === 0) {
formGroup.classList.remove('has-danger');
formGroup.classList.add('has-success');
element.classList.add('form-control-success');
}
}
}
}
export class FilterValueConverter{
toView(array, searchTerm) {
if(!searchTerm || searchTerm.length == 0) return array;
return array.filter((item) => {
return searchTerm && searchTerm.length > 0 ? this.itemMaches(searchTerm,item): true;
});
}
itemMaches(searchTerm, value){
let itemValue = value;
if(!itemValue) return false;
return itemValue.toUpperCase().indexOf(searchTerm.toUpperCase()) !== -1;
}
}
export class HighlightValueConverter{
toView(value, searchTerm) {
if(!searchTerm) return value;
return value.replace(new RegExp(searchTerm, 'gi'), `<b>$&</b>`);
}
}
<!doctype html>
<html>
<head>
<title>Aurelia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
rel="stylesheet" >
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
<link rel="stylesheet" href="style.css"></link
</head>
<body aurelia-app="main">
<h1>Loading...</h1>
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script>
<script src="https://freshcutdevelopment.github.io/rjs-bundle/node_modules/requirejs/require.js"></script>
<script src="https://freshcutdevelopment.github.io/rjs-bundle/config.js"></script>
<script src="https://freshcutdevelopment.github.io/rjs-bundle/bundles/aurelia.js"></script>
<script src="https://freshcutdevelopment.github.io/rjs-bundle/bundles/babel.js"></script>
<script>
require(['aurelia-bootstrapper']);
</script>
</body>
</html>
export function configure(aurelia) {
aurelia.use.standardConfiguration().plugin('aurelia-validation');
aurelia.start().then(() => aurelia.setRoot());
}
<template>
<ul class="ratings">
<li repeat.for="star of stars"
click.delegate="rate($index)"
mouseover.delegate="mouseOver($index) & debounce:100"
mouseout.delegate="mouseOut($index) & debounce:100">
<span class="star ${star.displayType === '' ? 'rated' : ''}"> <i class="fa fa-star${star.displayType}" aria-hidden="true"></i> </span>
</li>
</ul>
</template>
import {inject, bindable} from 'aurelia-framework';
@inject(Element) // Inject the instance of this element
export class StarRating{
@bindable rating;
constructor(element){
this.element = element;
this.stars = [
{ type: '-o', displayType: '-o', rated : false },
{ type: '-o', displayType: '-o', rated : false },
{ type: '-o', displayType: '-o', rated : false },
{ type: '-o', displayType: '-o', rated : false },
{ type: '-o', displayType: '-o', rated : false }
];
this.hovered = false;
}
bind(){
this.applyRating(this.rating);
}
applyRating(rating){
this.stars.forEach((star, index) =>
this.rateStar(star, rating, index));
}
rateStar(star, rating, index){
if(index < rating) this.toggleOn(star);
else {
this.toggleOff(star);
}
}
toggleOn(star){
star.displayType = '';
star.type = '';
star.rated = true;
}
toggleOff(star){
star.displayType = '-o';
star.type = '-o';
star.rated = false;
}
ratingFromIndex(index, star){
if(index === 0 && star.rated) return 0;
return index + 1;
}
rate(index){
let rating = this.ratingFromIndex(index, this.stars[0]);
this.rating = rating;
this.applyRating(rating);
this.raiseChangedEvent();
}
mouseOut(hoverIndex){
if(!this.hovered) return;
this.hovered = false;
this.applyHoverState(hoverIndex);
}
applyHoverState(hoverIndex){
this.stars.forEach((star, index) =>{
if(!this.shouldApplyHover(index, hoverIndex, star)) return;
if(this.hovered){
this.toggleDisplayOn(star);
}
else{
this.toggleDisplayOff(star);
}
});
}
mouseOver(hoverIndex){
if(this.hovered) return;
this.hovered = true;
this.applyHoverState(hoverIndex);
}
toggleDisplayOff(star){
star.displayType = star.type;
}
toggleDisplayOn(star){
star.displayType = '';
}
shouldApplyHover(starIndex, hoverIndex, star){
return starIndex <= hoverIndex && !star.rated;
}
raiseChangedEvent(){
let changeEvent = new CustomEvent('change', {rating: this.rating});
this.element.dispatchEvent(changeEvent);
}
}
.notification {
bottom: 5px;
position: fixed;
right: 5px;
background-color:#0d904f;
color:white;
padding:20px;
width:300px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
}
.auto-complete-wrapper{
}
.auto-complete.focused{
border: 2px solid #27ae60;
}
.auto-complete-menu{
position: absolute;
left: 2.5%;
z-index: 100;
width: 95%;
margin-bottom: 20px;
overflow: hidden;
background-color: #fff;
-webkit-border-radius: 8px;
-moz-border-radius: 8px;
border-radius: 8px;
box-shadow: 0px 0px 0px 1px green;
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
box-shadow: 0 5px 10px rgba(0,0,0,.2);
}
.auto-complete-menu li{
list-style-type:none;
}
.auto-complete-menu ul li:hover{
background-color: #2ecc71;
color:white;
}
/** ratings component **/
ul.ratings li{
display: inline;
list-style-type: none;
padding-right: 20px;
}
.star:hover{
cursor: pointer;
font-weight: bold;
}
.star.rated{
color:rgb(255, 204, 0);
}
<template>
<require from="filter"></require>
<require from="highlight"></require>
<ul show.bind="show" class="list-group">
<li class="list-group-item"
innerhtml.bind="item | highlight:searchTerm"
repeat.for="item of items | filter:searchTerm"
click.delegate="itemSelected(item)">
</li>
</ul>
</template>
import { inject, bindable, Container, ViewEngine, bindingMode} from 'aurelia-framework';
import { DOM } from 'aurelia-pal';
@inject(Element, Container, ViewEngine)
export class TypeaheadCustomAttribute{
@bindable items;
@bindable searchTerm;
@bindable show = false;
constructor(element, container, viewEngine) {
this.element = element;
this.container = container;
this.viewEngine = viewEngine;
this.keyPressListener = e => {
this.searchTerm = this.element.value;
this.show = true;
};
}
attached(){
this.wrapper = this.wrapWithDiv(this.element);
this.element.addEventListener('keyup', this.keyPressListener);
this.createTypeahead();
}
createTypeahead() {
this.viewEngine.loadViewFactory('typeahead-template.html').then(factory => {
let childContainer = this.container.createChild();
let view = factory.create(childContainer);
let autoCompleteMenu = DOM.createElement('div');
autoCompleteMenu.classList.add('auto-complete-menu');
this.autoCompleteMenu = autoCompleteMenu;
view.bind(this);
view.appendNodesTo(this.autoCompleteMenu);
this.wrapper.appendChild(autoCompleteMenu, this.element);
});
}
itemSelected(item){
this.searchTerm = item;
this.show = false;
let changeEvent = new CustomEvent('change', {detail: {value: item}});
this.element.dispatchEvent(changeEvent);
}
wrapWithDiv(){
var wrapper = DOM.createElement('div');
wrapper.classList.add('auto-complete-wrapper');
let sibling = this.element.nextElementSibling;
if(sibling){
this.element.parentElement.insertBefore(wrapper, sibling);
}
else{
this.element.parentElement.appendChild(wrapper);
}
wrapper.appendChild(this.element);
return wrapper;
}
removeAutocomplete() {
const body = DOM.querySelectorAll('body')[0];
body.removeChild(this.wrapper);
}
detached(){
this.element.removeEventListener('focus', this.focusListener);
this.element.removeEventListener('blur', this.blurListener);
this.element.removeEventListener('keyup', this.keyPressListener);
this.removeAutocomplete();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment