I was following this tutorial on how to build a whiteboard with react and konva and it provides an undo function for shapes but does not work for lines because lines are not added to the layer in the same way. How can I implement undo for free draw line?
EDIT:
To expand on my question, here is the relevant code:
I have a public repo that you can check out (and make a PR if that's easier).
https://github.com/ChristopherHButler/Sandbox-react-whiteboard
I have also have a demo you can try out here:
https://whiteboard-rho.now.sh/
Here is the relevant code
line component:
import Konva from "konva";
export const addLine = (stage, layer, mode = "brush") => {
let isPaint = false;
let lastLine;
stage.on("mousedown touchstart", function(e) {
isPaint = true;
let pos = stage.getPointerPosition();
lastLine = new Konva.Line({
stroke: mode == "brush" ? "red" : "white",
strokeWidth: mode == "brush" ? 5 : 20,
globalCompositeOperation:
mode === "brush" ? "source-over" : "destination-out",
points: [pos.x, pos.y],
draggable: mode == "brush",
});
layer.add(lastLine);
});
stage.on("mouseup touchend", function() {
isPaint = false;
});
stage.on("mousemove touchmove", function() {
if (!isPaint) {
return;
}
const pos = stage.getPointerPosition();
let newPoints = lastLine.points().concat([pos.x, pos.y]);
lastLine.points(newPoints);
layer.batchDraw();
});
};
HomePage component:
import React, { useState, createRef } from "react";
import { v1 as uuidv1 } from 'uuid';
import ButtonGroup from "react-bootstrap/ButtonGroup";
import Button from "react-bootstrap/Button";
import { Stage, Layer } from "react-konva";
import Rectangle from "../Shapes/Rectangle";
import Circle from "../Shapes/Circle";
import { addLine } from "../Shapes/Line";
import { addTextNode } from "../Shapes/Text";
import Image from "../Shapes/Image";
const HomePage = () => {
const [rectangles, setRectangles] = useState([]);
const [circles, setCircles] = useState([]);
const [images, setImages] = useState([]);
const [selectedId, selectShape] = useState(null);
const [shapes, setShapes] = useState([]);
const [, updateState] = useState();
const stageEl = createRef();
const layerEl = createRef();
const fileUploadEl = createRef();
const getRandomInt = max => {
return Math.floor(Math.random() * Math.floor(max));
};
const addRectangle = () => {
const rect = {
x: getRandomInt(100),
y: getRandomInt(100),
width: 100,
height: 100,
fill: "red",
id: `rect${rectangles.length + 1}`,
};
const rects = rectangles.concat([rect]);
setRectangles(rects);
const shs = shapes.concat([`rect${rectangles.length + 1}`]);
setShapes(shs);
};
const addCircle = () => {
const circ = {
x: getRandomInt(100),
y: getRandomInt(100),
width: 100,
height: 100,
fill: "red",
id: `circ${circles.length + 1}`,
};
const circs = circles.concat([circ]);
setCircles(circs);
const shs = shapes.concat([`circ${circles.length + 1}`]);
setShapes(shs);
};
const drawLine = () => {
addLine(stageEl.current.getStage(), layerEl.current);
};
const eraseLine = () => {
addLine(stageEl.current.getStage(), layerEl.current, "erase");
};
const drawText = () => {
const id = addTextNode(stageEl.current.getStage(), layerEl.current);
const shs = shapes.concat([id]);
setShapes(shs);
};
const drawImage = () => {
fileUploadEl.current.click();
};
const forceUpdate = React.useCallback(() => updateState({}), []);
const fileChange = ev => {
let file = ev.target.files[0];
let reader = new FileReader();
reader.addEventListener(
"load",
() => {
const id = uuidv1();
images.push({
content: reader.result,
id,
});
setImages(images);
fileUploadEl.current.value = null;
shapes.push(id);
setShapes(shapes);
forceUpdate();
},
false
);
if (file) {
reader.readAsDataURL(file);
}
};
const undo = () => {
const lastId = shapes[shapes.length - 1];
let index = circles.findIndex(c => c.id == lastId);
if (index != -1) {
circles.splice(index, 1);
setCircles(circles);
}
index = rectangles.findIndex(r => r.id == lastId);
if (index != -1) {
rectangles.splice(index, 1);
setRectangles(rectangles);
}
index = images.findIndex(r => r.id == lastId);
if (index != -1) {
images.splice(index, 1);
setImages(images);
}
shapes.pop();
setShapes(shapes);
forceUpdate();
};
document.addEventListener("keydown", ev => {
if (ev.code == "Delete") {
let index = circles.findIndex(c => c.id == selectedId);
if (index != -1) {
circles.splice(index, 1);
setCircles(circles);
}
index = rectangles.findIndex(r => r.id == selectedId);
if (index != -1) {
rectangles.splice(index, 1);
setRectangles(rectangles);
}
index = images.findIndex(r => r.id == selectedId);
if (index != -1) {
images.splice(index, 1);
setImages(images);
}
forceUpdate();
}
});
return (
<div className="home-page">
<ButtonGroup style={{ marginTop: '1em', marginLeft: '1em' }}>
<Button variant="secondary" onClick={addRectangle}>
Rectangle
</Button>
<Button variant="secondary" onClick={addCircle}>
Circle
</Button>
<Button variant="secondary" onClick={drawLine}>
Line
</Button>
<Button variant="secondary" onClick={eraseLine}>
Erase
</Button>
<Button variant="secondary" onClick={drawText}>
Text
</Button>
<Button variant="secondary" onClick={drawImage}>
Image
</Button>
<Button variant="secondary" onClick={undo}>
Undo
</Button>
</ButtonGroup>
<input
style={{ display: "none" }}
type="file"
ref={fileUploadEl}
onChange={fileChange}
/>
<Stage
style={{ margin: '1em', border: '2px solid grey' }}
width={window.innerWidth * 0.9}
height={window.innerHeight - 150}
ref={stageEl}
onMouseDown={e => {
// deselect when clicked on empty area
const clickedOnEmpty = e.target === e.target.getStage();
if (clickedOnEmpty) {
selectShape(null);
}
}}
>
<Layer ref={layerEl}>
{rectangles.map((rect, i) => {
return (
<Rectangle
key={i}
shapeProps={rect}
isSelected={rect.id === selectedId}
onSelect={() => {
selectShape(rect.id);
}}
onChange={newAttrs => {
const rects = rectangles.slice();
rects[i] = newAttrs;
setRectangles(rects);
}}
/>
);
})}
{circles.map((circle, i) => {
return (
<Circle
key={i}
shapeProps={circle}
isSelected={circle.id === selectedId}
onSelect={() => {
selectShape(circle.id);
}}
onChange={newAttrs => {
const circs = circles.slice();
circs[i] = newAttrs;
setCircles(circs);
}}
/>
);
})}
{images.map((image, i) => {
return (
<Image
key={i}
imageUrl={image.content}
isSelected={image.id === selectedId}
onSelect={() => {
selectShape(image.id);
}}
onChange={newAttrs => {
const imgs = images.slice();
imgs[i] = newAttrs;
}}
/>
);
})}
</Layer>
</Stage>
</div>
);
}
export default HomePage;
As a solution, you should just use the same react modal for lines. It is not recommended to create shape instances manually (like new Konva.Line) when you work with react-konva.
Just define your state and make a correct render() from it, as you do in HomePage component.
You may store all shapes in one array. Or use a separate for lines. So to draw lines in react-konva way you can do this:
const App = () => {
const [lines, setLines] = React.useState([]);
const isDrawing = React.useRef(false);
const handleMouseDown = (e) => {
isDrawing.current = true;
const pos = e.target.getStage().getPointerPosition();
setLines([...lines, [pos.x, pos.y]]);
};
const handleMouseMove = (e) => {
// no drawing - skipping
if (!isDrawing.current) {
return;
}
const stage = e.target.getStage();
const point = stage.getPointerPosition();
let lastLine = lines[lines.length - 1];
// add point
lastLine = lastLine.concat([point.x, point.y]);
// replace last
lines.splice(lines.length - 1, 1, lastLine);
setLines(lines.concat());
};
const handleMouseUp = () => {
isDrawing.current = false;
};
return (
<Stage
width={window.innerWidth}
height={window.innerHeight}
onMouseDown={handleMouseDown}
onMousemove={handleMouseMove}
onMouseup={handleMouseUp}
>
<Layer>
<Text text="Just start drawing" />
{lines.map((line, i) => (
<Line key={i} points={line} stroke="red" />
))}
</Layer>
</Stage>
);
};
Demo: https://codesandbox.io/s/hungry-architecture-v380jlvwrl?file=/index.js
Then the next step is how to implement undo/redo. You just need to keep a history of state changes. Take a look here for demo: https://konvajs.org/docs/react/Undo-Redo.html
If I understand this right you saying that for shapes which are added individually there is an easy 'undo' process, but for lines which use an array of points for their segments, there is no simple undo - and no code in the tutorial you are following?
I can't give you a react code sample but I can explain some of the concepts you need to code up.
The 'freehand line' in your whiteboard is created as a sequence of points. You mousedown and the first point is noted, then you move the mouse and on each movemove event that fires the current mouse position is added to the end of the array. By the time you complete the line and mouseup fires, you have thrown multiple points into the line array.
In the Konvajs line tutorial it states:
To define the path of the line you should use points property. If you
have three points with x and y coordinates you should define points
property as: [x1, y1, x2, y2, x3, y3].
[Because...] Flat array of numbers should work faster and use less memory than
array of objects.
So - your line points are added as separate values into the line.points array.
Now lets think about undo - you are probably there already but I'll write it out anyway - to undo a single segment of the line you need to erase the last 2 entries in the array. To erase the entire line - well you can use the standard shape.remove() or shape.destroy() methods.
In the following snippet the two buttons link to code to 'undo' lines. The 'Undo by segment' button shows how to pop the last two entries in the line.points array to remove a segment of the line, and the 'Undo by line' button removes entire lines. This is not a react example specifically, but you will in the end create something very close to this in your react case.
// Code to erase line one segment at a time.
$('#undosegment').on('click', function(){
// get the last line we added to the canvas - tracked via lines array in this demo
if (lines.length === 0){
return;
}
lastLine = lines[lines.length - 1];
let pointsArray = lastLine.points(); // get current points in line
if (pointsArray.length === 0){ // no more points so destroy this line object.
lastLine.destroy();
layer.batchDraw();
lines.pop(); // remove from our lines-tracking array.
return;
}
// remove last x & y entrie, pop appears to be fastest way to achieve AND adjust array length
pointsArray.pop(); // remove the last Y pos
pointsArray.pop(); // remove the last X pos
lastLine.points(pointsArray); // give the points back into the line
layer.batchDraw();
})
// Code to erase entire lines.
$('#undoline').on('click', function(){
// get the last line we added to the canvas - tracked via lines array in this demo
if (lines.length === 0){
return;
}
lastLine = lines[lines.length - 1];
lastLine.destroy(); // remove from our lines-tracking array.
lines.pop();
layer.batchDraw();
})
// code from here on is all about drawing the lines.
let
stage = new Konva.Stage({
container: 'container',
width: $('#container').width(),
height: $('#container').height()
}),
// add a layer to draw on
layer = new Konva.Layer();
stage.add(layer);
stage.draw();
let isPaint = false;
let lastLine;
let lines = [];
stage.on('mousedown', function(){
isPaint = true;
let pos = stage.getPointerPosition();
lastLine = new Konva.Line({ stroke: 'magenta', strokeWidth: 4, points: [pos.x, pos.y]});
layer.add(lastLine);
lines.push(lastLine);
})
stage.on("mouseup touchend", function() {
isPaint = false;
});
stage.on("mousemove touchmove", function() {
if (!isPaint) {
return;
}
const pos = stage.getPointerPosition();
let newPoints = lastLine.points().concat([pos.x, pos.y]);
lastLine.points(newPoints);
layer.batchDraw();
});
body {
margin: 10;
padding: 10;
overflow: hidden;
background-color: #f0f0f0;
}
#container {
border: 1px solid silver;
width: 500px;
height: 300px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#^3/konva.min.js"></script>
<p>Click and drag to draw a line </p>
<p>
<button id='undosegment'>Undo by segment</button> <button id='undoline'>Undo by line</button>
</p>
<div id="container"></div>
Very frustrating errors today. I've spent the entire day trying to debug my small application that works perfectly on my localhost, but errors out on heroku occasionally.
If I refresh the page several times, I can achieve the login. But it takes 2-3 refreshes.
The two errors I get when logging in a user are -
Uncaught TypeError: Cannot read property 'exercise_name' of undefined
And
Uncaught TypeError: Cannot read property '_currentElement' of null
Now I basically know what the issue is. I must not have my props when I initially try to map over them. One of the props is an array of exercises, with one of the keys as 'exercise_name.' I'm guessing it has to do with the speed I receive the from local host, compared to heroku's ajax calls.
Here is my issue,
I do not know which component this is coming from since, I use exercise_name in 4 components. Heroku has line numbers, but they are of no help since it doesn't point to anything in my application and I can't drop debuggers in heroku like I can on my machine here.
I've tried setting default props in mapStateToProps like so -
allExercises: state.entities.exercise || []
Did not work.
Ive tried wrapping things in conditionals in my components. Hasn't worked.
The following four components use exercise_name. Any direction would be greatly appreciated.
I understand the following is a lot of code. I would be completely content with an answer letting me know how to find which lines of code are producing these errors on heroku, or how to debug on heroku in general.
component 1
import React from 'react';
import { withRouter } from 'react-router-dom';
import SetResultContainer from '../setresult/create_setresult_container';
class ExerciseIndex extends React.Component {
constructor(props) {
super(props);
this.state = {
inputVal: '',
active: 'FIRST',
name: ''
};
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(e) {
this.setState({ inputVal: e.target.value })
}
componentDidMount() {
this.props.requestAllExercises();
}
handleClick(e) {
this.setState({ inputVal: e.currentTarget.attributes.value.value})
}
handleSubmit(e) {
let newActive = this.state.active === 'FIRST' ? 'SECOND' : null
let allExercises = this.props.allExercises;
let selected;
let name;
if (allExercises) {
allExercises.forEach(exercise => {
if (exercise.exercise_name === this.state.inputVal) {
selected = exercise,
name = exercise.exercise_name
}
})
e.preventDefault();
}
if (!name) {
this.setState({inputVal: 'Invalid Input, Please try Again'})
return 'Invalid Input'
}
this.props.requestExercise(selected)
this.setState({inputVal: '', active: newActive, name: name})
this.props.requestAllExercises();
}
render() {
let allExercises = this.props.allExercises || [{ exercise_name: '' }]
let match = allExercises.map((exercise) => {
if (this.state.inputVal === '') return [];
let matched = [];
if (this.state.inputVal.length > 0) {
for (var j = 0; j < this.state.inputVal.length; j++) {
matched = [];
if (exercise.exercise_name.slice(0, j + 1).toUpperCase() === this.state.inputVal.slice(0, j + 1).toUpperCase()) {
matched.push(<li onClick={this.handleClick}
value={exercise.exercise_name}
className="workout-auto-li"
key={exercise.id}>{exercise.exercise_name}</li>);
}
}
} else {
matched.push(<li onClick={this.handleClick}
value={exercise.exercise_name}
className="workout-auto-li"
key={exercise.id}>{exercise.exercise_name}</li>)
}
return matched;
});
return (
<div>
{this.props.allExercises ? (
<div>
{this.state.active === 'FIRST' ? (
<div className="exercise-main-div">
<div className="exercise-second-div">
<label className="exercise-label">
<h3>Add an Exercise for {this.props.liftname}</h3>
<input type="text" value={this.state.inputVal}
onChange={this.handleChange}
className="exercise-input"
/>
</label>
<ul className="exercise-ul">
{match}
</ul>
<button className="new-exercise-button"
onClick={this.handleSubmit}>Add Exercise</button>
</div>
</div>
) : this.state.active === 'SECOND' ? (
<SetResultContainer user={this.props.user}
exercises={this.props.exercises}
exercise={this.state.name}
liftname={this.props.liftname}/>
) : null }
</div>
) : null }
</div>
);
}
}
export default withRouter(ExerciseIndex);
component 2
import React from 'react';
import { withRouter } from 'react-router';
import values from 'lodash/values'
import { Pie } from 'react-chartjs-2';
class Leaderboard extends React.Component {
constructor(props) {
super(props)
this.state = { exercise: null }
this.handleUpdate = this.handleUpdate.bind(this)
}
componentDidMount() {
this.props.requestAllUsers();
this.props.requestAllExercises();
}
handleUpdate(property) {
return e => this.setState({ [property]: e.target.value });
}
render() {
const members = this.props.members.map(member => {
return <li className="members-list" key={member.id + 1}>{member.username}</li>
})
const memberId = {}
this.props.members.map(member => {
memberId[member.username] = member
})
const membersSetResults = {}
const membersLiftMaxes = {}
const completedMemberExercises = []
const completedExercises = {}
this.props.members.map(member => {
if (member.workouts) {
let workouts = values(member.workouts)
for (var i = 0; i < workouts.length; i++) {
let workoutResult = workouts[i].setresults
let results = values(workoutResult)
if (membersSetResults[member.username]) {
membersSetResults[member.username].unshift(results)
} else {
membersSetResults[member.username] = [results];
}
}
}
})
Object.keys(membersSetResults).map(member => {
let setResults = membersSetResults[member]
membersLiftMaxes[member] = {}
for (var i = 0; i < setResults.length; i++) {
let sets = setResults[i]
for (var j = 0; j < sets.length; j++) {
let currentExercise = this.props.allExercises[sets[j].exercise_id]
let exercise = currentExercise.exercise_name
if (completedMemberExercises.indexOf(exercise) < 0 && currentExercise.ex_type === 'lift') {
completedMemberExercises.push(exercise)
}
if (completedExercises[exercise]) {
completedExercises[exercise] += 1
} else if (!completedExercises[exercise]) {
completedExercises[exercise] = 1
}
if (currentExercise.ex_type === 'lift') {
if (membersLiftMaxes[member][exercise]) {
if(membersLiftMaxes[member][exercise] < sets[j].weight_lifted) {
membersLiftMaxes[member][exercise] = sets[j].weight_lifted
}
} else if (!membersLiftMaxes[member][exercise]) {
membersLiftMaxes[member][exercise] = sets[j].weight_lifted
}
}
}
}
})
const PieChart = {
datasets: [{
data: Object.values(completedExercises),
backgroundColor: [
'#2D4262',
'#363237',
'#73605B',
'#D09683',
'#F1F3CE',
'#1E656D',
'#00293C',
'#F0810F',
'#75B1A9',
],
}],
labels: Object.keys(completedExercises)
};
let exerciseDropdown = completedMemberExercises.map((exercise, idx) => {
return <option key={idx} value={exercise}>{exercise}</option>
})
let sorted = [];
const memberAndMax = {}
Object.keys(membersLiftMaxes).map(member => {
if (this.state.exercise) {
let exerciseMax = membersLiftMaxes[member][this.state.exercise]
if(!memberAndMax[this.state.exercise]){
memberAndMax[this.state.exercise] = []
memberAndMax[this.state.exercise].push([member, exerciseMax])
} else if (memberAndMax[this.state.exercise]) {
memberAndMax[this.state.exercise].push([member, exerciseMax])
}
memberAndMax[this.state.exercise].map(max => {
if (sorted.indexOf(max) < 0) {
if (max[1] > 0) {
sorted.push(max)
}
}
})
sorted.sort((a, b) => {
return a[1] - b[1]
})
}
})
let maxLis = sorted.reverse().map((user) => {
if (memberId[user[0]].id === this.props.cu.id) {
return <li className='userPresent' key={memberId[user[0]].id}>
<p className="members-list-p">{user[0]}</p>
<p className="members-list-p-two">{user[1]}</p></li>
} else {
return <li className='members-list' key={memberId[user[0]].id}>
<p className="members-list-p">{user[0]}</p>
<p className="members-list-p-two">{user[1]}</p></li>
}
})
return (
<div className='main-leaderboard'>
<div className='lb-reset-div'>
<button className='lb-reset-button' onClick={() => this.setState({exercise: null})}>Reset</button>
<select className='leaderboard-dropdown' onChange={this.handleUpdate('exercise')}>
<option>Please Select</option>
{exerciseDropdown}
</select>
</div>
{(this.state.exercise) ? (
<div className='lb-ul-div'>
<h3 className='selected-ex-title'>{this.state.exercise}</h3>
<ul className='leaderboard-ul'>
<li className="members-list"><p className="members-list-p">Name</p>
<p className="members-list-p-two">Max (lbs)</p></li>
{maxLis}
</ul>
</div>
): (!this.state.exercise) ? (
<div className='lb-ul-div'>
<h3 className='selected-ex-title'>Leaderboard</h3>
<ul className='leaderboard-ul'>
{members}
</ul>
</div>
): null}
<div className='pie-chart-div-lb'>
<h3 className='pie-chart-header'>What the World's Doing</h3>
<Pie circumfrence={300} data={PieChart}/>
</div>
</div>
)
}
}
export default withRouter(Leaderboard);
component 3
import React from 'react';
import { withRouter } from 'react-router';
import values from 'lodash/values';
import { Line, Pie } from 'react-chartjs-2';
class SearchBestWorkouts extends React.Component {
constructor(props) {
super(props);
this.state = {
inputVal: '',
name: '',
active: '',
result: ''
};
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
this.props.requestAllExercises();
this.setState({active: 'FIRST'})
}
handleChange(e) {
e.preventDefault(e)
this.setState({ inputVal: e.target.value })
}
handleClick(e) {
this.setState({ inputVal: e.currentTarget.attributes.value.value})
}
handleSubmit(e) {
let newActive = this.state.active === 'FIRST' ? 'SECOND' : 'FIRST'
let allExercises = values(this.props.exercises);
let selected;
let name;
if (newActive === 'SECOND') {
allExercises.forEach(exercise => {
if (exercise.exercise_name === this.state.inputVal) {
selected = exercise,
name = exercise.exercise_name
}
})
e.preventDefault();
if (!name) {
this.setState({inputVal: 'Invalid Input, Please try Again'})
return 'Invalid Input'
}
this.setState({inputVal: '', active: newActive, name: name})
this.props.requestAllExercises();
} else if (newActive === 'FIRST') {
this.setState({inputVal: '', active: newActive, name: '' })
}
}
render () {
let allWorkouts = this.props.allWorkouts;
let exercises = this.props.exercises;
let setResults = allWorkouts.map(workout => {
return values(workout.setresults)
})
let mergedSets = [].concat.apply([], setResults)
const allResults = {}
const exerciseTypes = {}
const completedExercises = {};
for (var i = 0; i < mergedSets.length; i++) {
let set = mergedSets[i];
let exercise = exercises[set.exercise_id]
let name = exercise.exercise_name
let bodypart = exercise.bodypart
if (exerciseTypes[bodypart]) {
exerciseTypes[bodypart] += 1
} else if (!exerciseTypes[bodypart]) {
exerciseTypes[bodypart] = 1
}
if (exercise.ex_type === 'lift') {
if (!allResults[name]) {
allResults[name] = { labels: [],
datasets: [{
label: 'Weight over Time',
backgroundColor: '#2988BC',
borderColor: '#2F496E',
data: [],
}],
};
}
if (completedExercises[name] < (set.weight_lifted)) {
completedExercises[name] = set.weight_lifted
} else if (!completedExercises[name]) {
completedExercises[name] = set.weight_lifted
}
allResults[name].labels.push(allResults[name].labels.length + 1)
allResults[name].datasets[0].data.unshift(set.weight_lifted)
}
}
const PieChart = {
datasets: [{
data: Object.values(exerciseTypes),
backgroundColor: [
'#2D4262', '#363237', '#73605B', '#D09683'
],
}],
labels: Object.keys(exerciseTypes)
};
const best = Object.keys(completedExercises).map((exercise) => {
if (this.state.inputVal === '') return [];
let bests = [];
if (this.state.inputVal.length > 0) {
for (var j = 0; j < this.state.inputVal.length; j++) {
bests = [];
if (exercise.slice(0, j + 1).toUpperCase() === this.state.inputVal.slice(0, j + 1).toUpperCase()) {
bests.push(<li onClick={this.handleClick}
value={exercise}
className="best-lift-li"
key={exercise.id}>{exercise}</li>);
}
}
} else {
bests.push(<li onClick={this.handleClick}
value={exercise}
className="best-lift-li"
key={exercise.id}>{exercise}</li>)
}
return bests;
});
return (
<div>
{this.state.active === 'FIRST' ? (
<div className="best-lift-div">
<div className='best-lift-div-two'>
<h3 className="best-lift-title">Personal Records</h3>
<div className='best-lift-input-div'>
<input type="text" value={this.state.inputVal}
onChange={this.handleChange}
className="best-lift"
placeholder="Enter an Exercise"
/>
</div>
<ul className='best-lift-ul'>
{best}
</ul>
<button className='best-lift-button' onClick={this.handleSubmit}>Best Lift</button>
</div>
</div>
) : this.state.active === 'SECOND' ? (
<div className="best-lift-div">
<div className='best-lift-div-two'>
<h3 className="best-lift-title">
{this.state.name}: {completedExercises[this.state.name]}</h3>
<div className='chart-background'>
<Line width={250} height={200} data={allResults[this.state.name]}/>
</div>
<button className='best-lift-button' onClick={this.handleSubmit}>Back</button>
</div>
<div className='best-lift-div-three'>
<h3 className="best-lift-title">Workout Analysis</h3>
<div className='pie-chart-background'>
<Pie circumfrence={100} data={PieChart} />
</div>
</div>
</div>
) : null}
</div>
)
}
}
export default withRouter(SearchBestWorkouts)
component 4
import React from 'react';
import values from 'lodash/values'
import InfiniteScroll from 'react-infinite-scroll-component';
import { withRouter } from 'react-router'
class WorkoutShow extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick (e) {
e.preventDefault();
this.props.deleteWorkout(this.props.selectedWorkout).then(
() => {
this.props.requestUser(this.props.match.params.userId)
}
)
this.props.toggleParent();
}
render () {
const setArray = values(this.props.selectedWorkout.setresults)
const exercises = this.props.exercises
const results = setArray.map((result, idx) => {
if (result.workout_id === this.props.selectedWorkout.id) {
return <li key={result.id} className='workout-show-li'>
<p className='workout-title'><p>Set {idx + 1}: </p><p>{exercises[result.exercise_id].exercise_name}</p></p>
<ul>
{result.weight_lifted ? (
<li className='workout-result-li'><p className='workout-result-li'>Weight:</p>{result.weight_lifted}{result.weight_unit}</li>
) : null}
{result.reps ? (
<li className='workout-result-li'><p className='workout-result-li'>Reps:</p>{result.reps}</li>
) : null}
{result.distance ? (
<li className='workout-result-li'><p className='workout-result-li'>Distance:</p>{result.distance}{result.distance_unit}</li>
) : null}
{result.hour || result.min || result.sec ? (
<li className='workout-result-li'><p className='workout-result-li'>Duration:</p>
<div className='dur-format'>
{result.hour ? (
<p className='dur-result-hour'>{result.hour}:</p>
) : null}
{result.min ? (
<p className='dur-result'>{result.min}:</p>
) : null}
{result.sec ? (
<p className='dur-result'>{result.sec}</p>
) : null}
</div>
</li>
) : null }
</ul>
</li>
}
})
return (
<div className="workout-show-main">
<h3 className="workout-show-title">{this.props.selectedWorkout.name}
<button className='remove-workout-button' onClick={this.handleClick}>DELETE</button></h3>
<InfiniteScroll>
<ul className="workout-show-ul">
{results}
</ul>
</InfiniteScroll>
</div>
);
}
}
export default withRouter(WorkoutShow);
Being on a server introduces delays in fetching of data, and things that
work perfectly locally don't work as well on the server.
The major culprit is your code, which is making assumptions about return values. For example
const setArray = values(this.props.selectedWorkout.setresults)
const exercises = this.props.exercises
const results = setArray.map((result, idx) => {
Line 3 of this block blindly assumes that setArray is defined and an array. You need to add checks in at every step, or provide default values (see the end of the first line below)
const setArray = values(this.props.selectedWorkout.setresults) || []
const exercises = this.props.exercises
const results = setArray.map((result, idx) => {
I'll leave it as an exercise for you to add what I call 'defensive code' to check return values and handle missing data without barfing.
You can also add try..catch blocks to trao any errors that your defensive code doesn't handle.
It will take some time, but it's worth upgrading your methodology to include this as standard practice if you want to write good code
I am having an issue with the angular ui bootstrap datepicker popup (https://angular-ui.github.io/bootstrap/) where I cannot get my model to update.
I have in place 2 different calendars - one with the popup and one without (uib-datepicker-popup and uib-datepicker) inside one of my angular component.
This is my code:
function headerCtrl () {
const self = this;
self.$onInit = () => {
self.dateOptions = {
showWeeks: false,
formatYear: 'yyyy',
startingDay: 1
};
self.today = function() {
self.calendar_date2 = new Date();
};
self.today();
self.clear = function() {
self.calendar_date2 = null;
};
self.inlineOptions = {
customClass: getDayClass,
minDate: new Date(),
showWeeks: true
};
self.toggleMin = function() {
self.inlineOptions.minDate = self.inlineOptions.minDate ? null : new Date();
self.dateOptions.minDate = self.inlineOptions.minDate;
};
self.toggleMin();
self.open = function() {
self.popup.opened = true;
};
self.setDate = function(year, month, day) {
self.calendar_date2 = new Date(year, month, day);
};
self.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];
self.format = self.formats[0];
self.altInputFormats = ['M!/d!/yyyy'];
self.popup = {
opened: false
};
var tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
var afterTomorrow = new Date();
afterTomorrow.setDate(tomorrow.getDate() + 1);
self.events = [
{
date: tomorrow,
status: 'full'
},
{
date: afterTomorrow,
status: 'partially'
}
];
function getDayClass(data) {
var date = data.date,
mode = data.mode;
if (mode === 'day') {
var dayToCheck = new Date(date).setHours(0,0,0,0);
for (var i = 0; i < self.events.length; i++) {
var currentDay = new Date(self.events[i].date).setHours(0,0,0,0);
if (dayToCheck === currentDay) {
return self.events[i].status;
}
}
}
}
self.changeCalendarDate = () => {
console.log('changeCalendarDate');
console.log(self.calendar_date);
};
self.changeCalendarDate2 = () => {
console.log('changeCalendarDate2');
console.log(self.calendar_date2);
};
}}
export default {
bindings: {
duration: "<",
zoom: "<",
selection: "<",
selections: "<",
calendar_date: "<",
onDurationChange: "&",
onCalendarDateChange: "&",
onHeatmapSelectionChange: "&"
},
controller: headerCtrl,
template: `
<div class="pp-header-container">
<div uib-datepicker
ng-model="$ctrl.calendar_date"
datepicker-options="$ctrl.dateOptions"
class="heatmap-header pp-sch-header-item"
ng-change="$ctrl.changeCalendarDate()"></div>
<div class="pp-header-calendar">
<input type="text"
uib-datepicker-popup="{{$ctrl.format}}"
ng-model="$ctrl.calendar_date2"
is-open="$ctrl.popup.opened"
datepicker-options="$ctrl.dateOptions"
ng-required="true"
close-text="Close"
alt-input-formats="$ctrl.altInputFormats"
maxlength="10"
size="10"
ng-change="$ctrl.changeCalendarDate2()" />
<button type="button" class="btn btn-default" ng-click="$ctrl.open()"><i class="glyphicon glyphicon-calendar"></i></button>
</div>
</div>`
}
http://prnt.sc/esq9c9
The calendar on the left (uib-datepicker) works, as soon as I pick a date, it triggers changeCalendarDate() and it prints the selected date (console.log(self.calendar_date);)
Now what I am trying to do is change calendar as I would like the one with the popup ( uib-datepicker-popup ) which does trigger changeCalendarDate2() but when I print the value (console.log(self.calendar_date2);) it is undefined.
I am sure I need to grab that value differently but I don't know how.
Can anyone help?
Thanks :)
I have solved this problem :D
The problem was this
uib-datepicker-popup="{{$ctrl.format}}"
as soon as I have removed thee value assigned and left only
uib-datepicker-popup
it worked. I don't know why but I am happier now :)
I am looking for autocomplete form for MaterializeCss, any plugins for this? i has try to use select2 but that's css not looks good
Materialize is an awesome library, I was able to get it to work.
$('document').ready(function() {
var input_selector = 'input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel], input[type=number], input[type=search], textarea';
/**************************
* Auto complete plugin *
*************************/
$(input_selector).each(function() {
var $input = $(this);
if ($input.hasClass('autocomplete')) {
var $array = $input.data('array'),
$inputDiv = $input.closest('.input-field'); // Div to append on
// Check if "data-array" isn't empty
if ($array !== '') {
// Create html element
var $html = '<ul class="autocomplete-content hide">';
for (var i = 0; i < $array.length; i++) {
// If path and class aren't empty add image to auto complete else create normal element
if ($array[i]['path'] !== '' && $array[i]['path'] !== undefined && $array[i]['path'] !== null && $array[i]['class'] !== undefined && $array[i]['class'] !== '') {
$html += '<li class="autocomplete-option"><img src="' + $array[i]['path'] + '" class="' + $array[i]['class'] + '"><span>' + $array[i]['value'] + '</span></li>';
} else {
$html += '<li class="autocomplete-option"><span>' + $array[i]['value'] + '</span></li>';
}
}
$html += '</ul>';
$inputDiv.append($html); // Set ul in body
// End create html element
function highlight(string) {
$('.autocomplete-content li').each(function() {
var matchStart = $(this).text().toLowerCase().indexOf("" + string.toLowerCase() + ""),
matchEnd = matchStart + string.length - 1,
beforeMatch = $(this).text().slice(0, matchStart),
matchText = $(this).text().slice(matchStart, matchEnd + 1),
afterMatch = $(this).text().slice(matchEnd + 1);
$(this).html("<span>" + beforeMatch + "<span class='highlight'>" + matchText + "</span>" + afterMatch + "</span>");
});
}
// Perform search
$(document).on('keyup', $input, function() {
var $val = $input.val().trim(),
$select = $('.autocomplete-content');
// Check if the input isn't empty
$select.css('width',$input.width());
if ($val != '') {
$select.children('li').addClass('hide');
$select.children('li').filter(function() {
$select.removeClass('hide'); // Show results
// If text needs to highlighted
if ($input.hasClass('highlight-matching')) {
highlight($val);
}
var check = true;
for (var i in $val) {
if ($val[i].toLowerCase() !== $(this).text().toLowerCase()[i])
check = false;
};
return check ? $(this).text().toLowerCase().indexOf($val.toLowerCase()) !== -1 : false;
}).removeClass('hide');
} else {
$select.children('li').addClass('hide');
}
});
// Set input value
$('.autocomplete-option').click(function() {
$input.val($(this).text().trim());
$('.autocomplete-option').addClass('hide');
});
} else {
return false;
}
}
});
});
.autocomplete-content {
position: absolute;
background: #383838;
margin-top: -.9rem;
}
.autocomplete-content li {
clear: both;
color: rgba(0, 0, 0, 0.87);
cursor: pointer;
line-height: 0;
width: 100%;
text-align: left;
text-transform: none;
padding: 10px;
}
.autocomplete-content li > span {
color: #ffa726;
font-size: .9rem;
padding: 1.2rem;
display: block;
}
.autocomplete-content li > span .highlight {
color: #000000;
}
.autocomplete-content li img {
height: 52px;
width: 52px;
padding: 5px;
margin: 0 15px;
}
.autocomplete-content li:hover {
background: #eee;
cursor: pointer;
}
.autocomplete-content > li:hover {
background: #292929;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.5/css/materialize.css" rel="stylesheet" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div class="row">
<div class="input-field col s12">
<label class="active">State</label>
<input type="text" id="autocompleteState" class="autocomplete inputFields">
</div>
</div>
<script>
var stateData = [{
value: "Alabama"
}, {
value: "Alaska"
}, {
value: "Arizona"
}, {
value: "Arkansas"
}, {
value: "California"
}, {
value: "Colorado"
}, {
value: "Connecticut"
}, {
value: "District of Columbia"
}, {
value: "Delaware"
}, {
value: "Florida"
}, {
value: "Georgia"
}, {
value: "Hawaii"
}, {
value: "Idaho"
}, {
value: "Illinois"
}, {
value: "Indiana"
}, {
value: "Iowa"
}, {
value: "Kansas"
}, {
value: "Kentucky"
}, {
value: "Louisiana"
}, {
value: "Maine"
}, ];
$('#autocompleteState').data('array', stateData);
</script>
Hope this helps people who are new to this just like me.:)
UPDATED 1/09/2016:
Autocomplete is already available officially:
http://materializecss.com/forms.html#autocomplete
I was looking for exactly the same and I think We have been lucky. They added the autocomplete recently, (not yet in the documentation). But you can see the info here https://github.com/SuperDJ/materialize/commit/3648f74542e41c3b3be4596870b7485f6ebdf176#diff-e4535828acef79852aa104417c16fe3dR157
and the code:
https://github.com/SuperDJ/materialize/blob/master/bin/materialize.css
https://github.com/SuperDJ/materialize/blob/master/bin/materialize.js
(function (root, factory) {
if(typeof module === 'object' && module.exports) {
module.exports = factory(require('jquery'));
} else if(typeof define === 'function' && define.amd) {
define(['jquery'], factory);
} else {
factory(root.jQuery);
}
}(this, function ($) {
var template = function (text) {
var matcher = new RegExp('<%=([\\s\\S]+?)%>|<%([\\s\\S]+?)%>|$', 'g');
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;
var escapeChar = function(match) {
return '\\' + escapes[match];
};
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, interpolate, evaluate, offset) {
source += text.slice(index, offset).replace(escapeRegExp, escapeChar);
index = offset + match.length;
if (match == "<% item.value %>")
interpolate = " item.value ";
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
} else if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
return match;
});
source += "';\n";
source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + 'return __p;\n';
var render;
try {
render = new Function('obj', source);
} catch (e) {
e.source = source;
throw e;
}
var _template = function(data) {
return render.call(this, data);
};
_template.source = 'function(obj){\n' + source + '}';
return _template;
};
var Autocomplete = function (el, options) {
this.options = $.extend(true, {}, Autocomplete.defaults, options);
this.$el = $(el);
this.$wrapper = this.$el.parent();
this.compiled = {};
this.$dropdown = null;
this.$appender = null;
this.$hidden = null;
this.resultCache = {};
this.value = '';
this.initialize();
};
Autocomplete.defaults = {
cacheable: true,
limit: 10,
multiple: {
enable: false,
maxSize: 4,
onExist: function (item) {
Materialize.toast('Tag: ' + item.text + '(' + item.id + ') is already added!', 2000);
},
onExceed: function (maxSize, item) {
Materialize.toast('Can\'t add over ' + maxSize + ' tags!', 2000);
},
onAppend: function (item) {
var self = this;
self.$el.removeClass('active');
self.$el.click();
},
onRemove: function (item) {
var self = this;
self.$el.removeClass('active');
self.$el.click();
}
},
hidden: {
enable: true,
el: '',
inputName: '',
required: false
},
appender: {
el: '',
tagName: 'ul',
className: 'ac-appender',
tagTemplate: '<div class="chip" data-id="<%= item.id %>" data-value="<% item.value %> data-text="<% item.text %>" "><%= item.text %>(<%= item.id %>)<i class="material-icons close">close</i></div>'
},
dropdown: {
el: '',
tagName: 'ul',
className: 'ac-dropdown',
itemTemplate: '<li class="ac-item" data-id="<%= item.id %>" data-value="<% item.value %>" data-text="<%= item.text %>" ><%= item.text %></li>',
noItem: ''
},
handlers: {
'setValue': '.ac-dropdown .ac-item',
'.ac-appender .ac-tag .close': 'remove'
},
getData: function (value, callback) {
callback(value, []);
},
onSelect: function (item) { },
onRemove: function (item) { alert('delete'); },
ignoreCase: true,
throttling: true,
showListOnFocus: false,
minLength:0
};
Autocomplete.prototype = {
constructor: Autocomplete,
initialize: function () {
var self = this;
var timer;
var fetching = false;
function getItemsHtml (list) {
var itemsHtml = '';
if (!list.length) {
return self.options.dropdown.noItem;
}
list.forEach(function (item, idx) {
if (idx >= self.options.limit) {
return false;
}
itemsHtml += self.compiled.item({ 'item': item});
});
return itemsHtml;
}
function handleList (value, list) {
var itemsHtml = getItemsHtml(list);
var currentValue = self.$el.val();
if (self.options.ignoreCase) {
currentValue = currentValue.toUpperCase();
}
if (self.options.cacheable && !self.resultCache.hasOwnProperty(value)) {
self.resultCache[value] = list;
}
if (value !== currentValue) {
return false;
}
if(itemsHtml) {
self.$dropdown.html(itemsHtml);
self.$dropdown.show();
} else {
self.$dropdown.hide();
}
}
self.value = self.options.multiple.enable ? [] : '';
self.compiled.tag = template(self.options.appender.tagTemplate);
self.compiled.item = template(self.options.dropdown.itemTemplate);
self.render();
if (self.options.showListOnFocus) {
self.$el.on('focus', function (e) {
if (self.options.throttling) {
clearTimeout(timer);
timer = setTimeout(function () {
self.options.getData('', handleList);
}, 200);
return true;
}
// self.$dropdown.show();
});
}
self.$el.on('input', function (e) {
var $t = $(this);
var value = $t.val();
if (!value) {
self.$dropdown.hide();
return false;
}
if (self.options.ignoreCase) {
value = value.toUpperCase();
}
if (self.resultCache.hasOwnProperty(value) && self.resultCache[value]) {
handleList(value, self.resultCache[value]);
return true;
}
if (self.options.showListOnFocus || self.options.minLength <= value.length) {
if (self.options.throttling) {
clearTimeout(timer);
timer = setTimeout(function () {
self.options.getData(value, handleList);
}, 200);
return true;
}
self.options.getData(value, handleList);
}
});
self.$el.on('keydown', function (e) {
var $t = $(this);
var keyCode = e.keyCode;
var $items, $hover;
// BACKSPACE KEY
if (keyCode == '8' && !$t.val()) {
if (!self.options.multiple.enable) {
return true;
}
if (!self.value.length) {
return true;
}
var lastItem = self.value[self.value.length - 1];
self.remove(lastItem);
return false;
}
// UP DOWN ARROW KEY
if (keyCode == '38' || keyCode == '40') {
$items = self.$dropdown.find('[data-id]');
if (!$items.size()) {
return false;
}
$hover = $items.filter('.ac-hover');
if (!$hover.size()) {
$items.removeClass('ac-hover');
$items.eq(keyCode == '40' ? 0 : -1).addClass('ac-hover');
} else {
var index = $hover.index();
$items.removeClass('ac-hover');
$items.eq(keyCode == '40' ? (index + 1) % $items.size() : index - 1).addClass('ac-hover');
}
return false;
}
// ENTER KEY CODE
if (keyCode == '13') {
$items = self.$dropdown.find('[data-id]');
if (!$items.size()) {
return false;
}
$hover = $items.filter('.ac-hover');
if (!$hover.size()) {
return false;
}
self.setValue({
id: $hover.data('id'),
text: $hover.data('text'),
value: $hover.data('value')
});
return false;
}
});
self.$dropdown.on('click', '[data-id]', function (e) {
var $t = $(this);
var item = {
id: $t.data('id'),
text: $t.data('text'),
value : $t.data('value')
};
self.setValue(item);
});
self.$appender.on('click', '[data-id] .close', function (e) {
var $t = $(this);
var $li = $t.closest('[data-id');
var item = {
id: $li.data('id'),
text: $li.data('text'),
value:$t.data('value')
};
self.remove(item);
});
},
render: function () {
var self = this;
if (self.options.dropdown.el) {
self.$dropdown = $(self.options.dropdown.el);
} else {
self.$dropdown = $(document.createElement(self.options.dropdown.tagName));
self.$dropdown.insertAfter(self.$el);
}
self.$dropdown.addClass(self.options.dropdown.className);
if (self.options.appender.el) {
self.$appender = $(self.options.appender.el);
} else {
self.$appender = $(document.createElement(self.options.appender.tagName));
self.$appender.insertBefore(self.$el);
}
if (self.options.hidden.enable) {
if (self.options.hidden.el) {
self.$hidden = $(self.options.hidden.el);
} else {
self.$hidden = $('<input type="hidden" class="validate" />');
self.$wrapper.append(self.$hidden);
}
if (self.options.hidden.inputName) {
self.$el.attr('name', self.options.hidden.inputName);
}
if (self.options.hidden.required) {
self.$hidden.attr('required', 'required');
}
}
self.$appender.addClass(self.options.appender.className);
},
setValue: function (item) {
var self = this;
if (self.options.multiple.enable) {
self.append(item);
} else {
self.select(item);
}
},
append: function (item) {
var self = this;
var $tag = self.compiled.tag({ 'item': item });
if (self.value.some(function (selectedItem) {
return selectedItem.id === item.id;
})) {
if ('function' === typeof self.options.multiple.onExist) {
self.options.multiple.onExist.call(this, item);
}
return false;
}
if (self.value.length >= self.options.multiple.maxSize) {
if ('function' === typeof self.options.multiple.onExceed) {
self.options.multiple.onExceed.call(this, self.options.multiple.maxSize);
}
return false;
}
self.value.push(item);
self.$appender.append($tag);
var valueStr = self.value.map(function (selectedItem) {
return selectedItem.id;
}).join(',');
if (self.options.hidden.enable) {
self.$hidden.val(valueStr);
}
self.$el.val('');
self.$el.data('value', valueStr);
self.$dropdown.html('').hide();
if ('function' === typeof self.options.multiple.onAppend) {
self.options.multiple.onAppend.call(self, item);
}
},
remove: function (item) {
var self = this;
self.$appender.find('[data-id="' + item.id + '"]').remove();
self.value = self.value.filter(function (selectedItem) {
return selectedItem.id !== item.id;
});
var valueStr = self.value.map(function (selectedItem) {
return selectedItem.id;
}).join(',');
if (self.options.hidden.enable) {
self.$hidden.val(valueStr);
self.$el.data('value', valueStr);
}
self.$dropdown.html('').hide();
if ('function' === typeof self.options.multiple.onRemove) {
self.options.multiple.onRemove.call(self, item);
}
self.options.onRemove(item);
},
select: function (item) {
var self = this;
self.value = item.text;
self.$el.val(item.text);
self.$el.data('value', item.id);
self.$dropdown.html('').hide();
if (self.options.hidden.enable) {
self.$hidden.val(item.id);
}
self.options.onSelect(item);
}
};
$.fn.materialize_autocomplete = function (options) {
var el = this;
var $el = $(el).eq(0);
var instance = $el.data('autocomplete');
if (instance && arguments.length) {
return instance;
}
var autocomplete = new Autocomplete(el, options);
$el.data('autocomplete', autocomplete);
$el.dropdown();
return autocomplete;
};
}));
down load this js and save inside your js folder jquery.materialize-autocomplete.js and u can over ride onSelect,minLength,showListOnFocus
Visit the link for demo https://ampersandacademy.com/demo/autocomplete/
you can easily achieve the autocomplete functionality using the Devberidge plugin.
Get Devbridge plugin using https://github.com/devbridge/jQuery-Autocomplete
<script type="text/javascript">
$(document).ready(function() {
$("#country").devbridgeAutocomplete({
//lookup: countries,
serviceUrl:"getCountry.php", //tell the script where to send requests
type:'POST',
//callback just to show it's working
onSelect: function (suggestion) {
// $('#selection').html('You selected: ' + suggestion.value + ', ' + suggestion.data);
},
showNoSuggestionNotice: true,
noSuggestionNotice: 'Sorry, no matching results',
});
});
Here the getCountry.php file returns the JSON data. For more info visit https://ampersandacademy.com/tutorials/materialize-css/materialize-css-framework-ajax-autocomplete-example-using-php
According to the commit done by SuperDJ (https://goo.gl/0Mbrtg), I didn't manage to get it works...
Here is the code :
<div class="row">
<div class="col s12">
<div class="row">
<div class="input-field col s12">
<input type="text" id="autocomplete" class="autocomplete highlight-matching" data-array='[{"value": "example","path": "","class": ""},{"value": "example 2","path": "","class": ""},{"value": "test","path": "","class": ""}]'>
<label for="autocomplete">Autocomplete</label>
</div>
</div>
</div>
</div>
Here is a codepen test :
http://codepen.io/anthonyvialleton/pen/BjPjKM
Need some help to get this work.
As per, here.
You just need to do this simple thing (From the example there only):
HTML:
<div class="row">
<div class="col s12">
<div class="row">
<div class="input-field col s12">
<i class="material-icons prefix">textsms</i>
<input type="text" id="autocomplete-input" class="autocomplete">
<label for="autocomplete-input">Autocomplete</label>
</div>
</div>
</div>
JS:
$('input.autocomplete').autocomplete({
data: {
"Apple": null,
"Microsoft": null,
"Google": null
}
});
(function (root, factory) {
if(typeof module === 'object' && module.exports) {
module.exports = factory(require('jquery'));
} else if(typeof define === 'function' && define.amd) {
define(['jquery'], factory);
} else {
factory(root.jQuery);
}
}(this, function ($) {
var template = function (text) {
var matcher = new RegExp('<%=([\\s\\S]+?)%>|<%([\\s\\S]+?)%>|$', 'g');
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;
var escapeChar = function(match) {
return '\\' + escapes[match];
};
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, interpolate, evaluate, offset) {
source += text.slice(index, offset).replace(escapeRegExp, escapeChar);
index = offset + match.length;
if (match == "<% item.value %>")
interpolate = " item.value ";
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
} else if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
return match;
});
source += "';\n";
source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + 'return __p;\n';
var render;
try {
render = new Function('obj', source);
} catch (e) {
e.source = source;
throw e;
}
var _template = function(data) {
return render.call(this, data);
};
_template.source = 'function(obj){\n' + source + '}';
return _template;
};
var Autocomplete = function (el, options) {
this.options = $.extend(true, {}, Autocomplete.defaults, options);
this.$el = $(el);
this.$wrapper = this.$el.parent();
this.compiled = {};
this.$dropdown = null;
this.$appender = null;
this.$hidden = null;
this.resultCache = {};
this.value = '';
this.initialize();
};
Autocomplete.defaults = {
cacheable: true,
limit: 10,
multiple: {
enable: false,
maxSize: 4,
onExist: function (item) {
Materialize.toast('Tag: ' + item.text + '(' + item.id + ') is already added!', 2000);
},
onExceed: function (maxSize, item) {
Materialize.toast('Can\'t add over ' + maxSize + ' tags!', 2000);
},
onAppend: function (item) {
var self = this;
self.$el.removeClass('active');
self.$el.click();
},
onRemove: function (item) {
var self = this;
self.$el.removeClass('active');
self.$el.click();
}
},
hidden: {
enable: true,
el: '',
inputName: '',
required: false
},
appender: {
el: '',
tagName: 'ul',
className: 'ac-appender',
tagTemplate: '<div class="chip" data-id="<%= item.id %>" data-value="<% item.value %> data-text="<% item.text %>" "><%= item.text %>(<%= item.id %>)<i class="material-icons close">close</i></div>'
},
dropdown: {
el: '',
tagName: 'ul',
className: 'ac-dropdown',
itemTemplate: '<li class="ac-item" data-id="<%= item.id %>" data-value="<% item.value %>" data-text="<%= item.text %>" ><%= item.text %></li>',
noItem: ''
},
handlers: {
'setValue': '.ac-dropdown .ac-item',
'.ac-appender .ac-tag .close': 'remove'
},
getData: function (value, callback) {
callback(value, []);
},
onSelect: function (item) { },
onRemove: function (item) { alert('delete'); },
ignoreCase: true,
throttling: true,
showListOnFocus: false,
minLength:0
};
Autocomplete.prototype = {
constructor: Autocomplete,
initialize: function () {
var self = this;
var timer;
var fetching = false;
function getItemsHtml (list) {
var itemsHtml = '';
if (!list.length) {
return self.options.dropdown.noItem;
}
list.forEach(function (item, idx) {
if (idx >= self.options.limit) {
return false;
}
itemsHtml += self.compiled.item({ 'item': item});
});
return itemsHtml;
}
function handleList (value, list) {
var itemsHtml = getItemsHtml(list);
var currentValue = self.$el.val();
if (self.options.ignoreCase) {
currentValue = currentValue.toUpperCase();
}
if (self.options.cacheable && !self.resultCache.hasOwnProperty(value)) {
self.resultCache[value] = list;
}
if (value !== currentValue) {
return false;
}
if(itemsHtml) {
self.$dropdown.html(itemsHtml);
self.$dropdown.show();
} else {
self.$dropdown.hide();
}
}
self.value = self.options.multiple.enable ? [] : '';
self.compiled.tag = template(self.options.appender.tagTemplate);
self.compiled.item = template(self.options.dropdown.itemTemplate);
self.render();
if (self.options.showListOnFocus) {
self.$el.on('focus', function (e) {
if (self.options.throttling) {
clearTimeout(timer);
timer = setTimeout(function () {
self.options.getData('', handleList);
}, 200);
return true;
}
// self.$dropdown.show();
});
}
self.$el.on('input', function (e) {
var $t = $(this);
var value = $t.val();
if (!value) {
self.$dropdown.hide();
return false;
}
if (self.options.ignoreCase) {
value = value.toUpperCase();
}
if (self.resultCache.hasOwnProperty(value) && self.resultCache[value]) {
handleList(value, self.resultCache[value]);
return true;
}
if (self.options.showListOnFocus || self.options.minLength <= value.length) {
if (self.options.throttling) {
clearTimeout(timer);
timer = setTimeout(function () {
self.options.getData(value, handleList);
}, 200);
return true;
}
self.options.getData(value, handleList);
}
});
self.$el.on('keydown', function (e) {
var $t = $(this);
var keyCode = e.keyCode;
var $items, $hover;
// BACKSPACE KEY
if (keyCode == '8' && !$t.val()) {
if (!self.options.multiple.enable) {
return true;
}
if (!self.value.length) {
return true;
}
var lastItem = self.value[self.value.length - 1];
self.remove(lastItem);
return false;
}
// UP DOWN ARROW KEY
if (keyCode == '38' || keyCode == '40') {
$items = self.$dropdown.find('[data-id]');
if (!$items.size()) {
return false;
}
$hover = $items.filter('.ac-hover');
if (!$hover.size()) {
$items.removeClass('ac-hover');
$items.eq(keyCode == '40' ? 0 : -1).addClass('ac-hover');
} else {
var index = $hover.index();
$items.removeClass('ac-hover');
$items.eq(keyCode == '40' ? (index + 1) % $items.size() : index - 1).addClass('ac-hover');
}
return false;
}
// ENTER KEY CODE
if (keyCode == '13') {
$items = self.$dropdown.find('[data-id]');
if (!$items.size()) {
return false;
}
$hover = $items.filter('.ac-hover');
if (!$hover.size()) {
return false;
}
self.setValue({
id: $hover.data('id'),
text: $hover.data('text'),
value: $hover.data('value')
});
return false;
}
});
self.$dropdown.on('click', '[data-id]', function (e) {
var $t = $(this);
var item = {
id: $t.data('id'),
text: $t.data('text'),
value : $t.data('value')
};
self.setValue(item);
});
self.$appender.on('click', '[data-id] .close', function (e) {
var $t = $(this);
var $li = $t.closest('[data-id');
var item = {
id: $li.data('id'),
text: $li.data('text'),
value:$t.data('value')
};
self.remove(item);
});
},
render: function () {
var self = this;
if (self.options.dropdown.el) {
self.$dropdown = $(self.options.dropdown.el);
} else {
self.$dropdown = $(document.createElement(self.options.dropdown.tagName));
self.$dropdown.insertAfter(self.$el);
}
self.$dropdown.addClass(self.options.dropdown.className);
if (self.options.appender.el) {
self.$appender = $(self.options.appender.el);
} else {
self.$appender = $(document.createElement(self.options.appender.tagName));
self.$appender.insertBefore(self.$el);
}
if (self.options.hidden.enable) {
if (self.options.hidden.el) {
self.$hidden = $(self.options.hidden.el);
} else {
self.$hidden = $('<input type="hidden" class="validate" />');
self.$wrapper.append(self.$hidden);
}
if (self.options.hidden.inputName) {
self.$el.attr('name', self.options.hidden.inputName);
}
if (self.options.hidden.required) {
self.$hidden.attr('required', 'required');
}
}
self.$appender.addClass(self.options.appender.className);
},
setValue: function (item) {
var self = this;
if (self.options.multiple.enable) {
self.append(item);
} else {
self.select(item);
}
},
append: function (item) {
var self = this;
var $tag = self.compiled.tag({ 'item': item });
if (self.value.some(function (selectedItem) {
return selectedItem.id === item.id;
})) {
if ('function' === typeof self.options.multiple.onExist) {
self.options.multiple.onExist.call(this, item);
}
return false;
}
if (self.value.length >= self.options.multiple.maxSize) {
if ('function' === typeof self.options.multiple.onExceed) {
self.options.multiple.onExceed.call(this, self.options.multiple.maxSize);
}
return false;
}
self.value.push(item);
self.$appender.append($tag);
var valueStr = self.value.map(function (selectedItem) {
return selectedItem.id;
}).join(',');
if (self.options.hidden.enable) {
self.$hidden.val(valueStr);
}
self.$el.val('');
self.$el.data('value', valueStr);
self.$dropdown.html('').hide();
if ('function' === typeof self.options.multiple.onAppend) {
self.options.multiple.onAppend.call(self, item);
}
},
remove: function (item) {
var self = this;
self.$appender.find('[data-id="' + item.id + '"]').remove();
self.value = self.value.filter(function (selectedItem) {
return selectedItem.id !== item.id;
});
var valueStr = self.value.map(function (selectedItem) {
return selectedItem.id;
}).join(',');
if (self.options.hidden.enable) {
self.$hidden.val(valueStr);
self.$el.data('value', valueStr);
}
self.$dropdown.html('').hide();
if ('function' === typeof self.options.multiple.onRemove) {
self.options.multiple.onRemove.call(self, item);
}
self.options.onRemove(item);
},
select: function (item) {
var self = this;
self.value = item.text;
self.$el.val(item.text);
self.$el.data('value', item.id);
self.$dropdown.html('').hide();
if (self.options.hidden.enable) {
self.$hidden.val(item.id);
}
self.options.onSelect(item);
}
};
$.fn.materialize_autocomplete = function (options) {
var el = this;
var $el = $(el).eq(0);
var instance = $el.data('autocomplete');
if (instance && arguments.length) {
return instance;
}
var autocomplete = new Autocomplete(el, options);
$el.data('autocomplete', autocomplete);
$el.dropdown();
return autocomplete;
};
}));
You can use the above .js file and you can over ride
Onselect
showListOnFocus: false,
minLength:2
according to your requirement and it will work.
Just write the Initialisation script inside $(document).ready(function(){});
i.e.
$(document).ready(function(){
$('input.autocomplete').autocomplete({
data: {
"Apple": null,
"Microsoft": null,
"Google": null
}
});
});
As mentioned above, autocomplete has been added the the materialize framework but it is still pretty limited. I'm waiting for a solution that supports values (ex Ids) icons and text.
Re: https://github.com/Dogfalo/materialize/issues/4086
How to upload file in SAP Netweaver server using SAPUI5? I tried to upload file using FileUploader but did not get the luck if any one can help it will be very appreciated.
Thanks in Advance
Nothing was added to the manifest nor the component nor index files. It is working for me, you just need to change the number of columns to whatever you want to fit your file.
UploadFile.view.xml
<VBox>
<sap.ui.unified:FileUploader id="idfileUploader" typeMissmatch="handleTypeMissmatch" change="handleValueChange" maximumFileSize="10" fileSizeExceed="handleFileSize" maximumFilenameLength="50" filenameLengthExceed="handleFileNameLength" multiple="false" width="50%" sameFilenameAllowed="false" buttonText="Browse" fileType="CSV" style="Emphasized" placeholder="Choose a CSV file"/>
<Button text="Upload your file" press="onUpload" type="Emphasized"/>
</VBox>
UploadFile.controller.js
sap.ui.define(["sap/ui/core/mvc/Controller", "sap/m/MessageToast", "sap/m/MessageBox", "sap/ui/core/routing/History"], function(
Controller, MessageToast, MessageBox, History) {
"use strict";
return Controller.extend("cafeteria.controller.EmployeeFileUpload", {
onNavBack: function() {
var oHistory = History.getInstance();
var sPreviousHash = oHistory.getPreviousHash();
if (sPreviousHash !== undefined) {
window.history.go(-1);
} else {
var oRouter = sap.ui.core.UIComponent.getRouterFor(this);
oRouter.navTo("admin", true);
}
},
handleTypeMissmatch: function(oEvent) {
var aFileTypes = oEvent.getSource().getFileType();
jQuery.each(aFileTypes, function(key, value) {
aFileTypes[key] = "*." + value;
});
var sSupportedFileTypes = aFileTypes.join(", ");
MessageToast.show("The file type *." + oEvent.getParameter("fileType") +
" is not supported. Choose one of the following types: " +
sSupportedFileTypes);
},
handleValueChange: function(oEvent) {
MessageToast.show("Press 'Upload File' to upload file '" + oEvent.getParameter("newValue") + "'");
},
handleFileSize: function(oEvent) {
MessageToast.show("The file size should not exceed 10 MB.");
},
handleFileNameLength: function(oEvent) {
MessageToast.show("The file name should be less than that.");
},
onUpload: function(e) {
var oResourceBundle = this.getView().getModel("i18n").getResourceBundle();
var fU = this.getView().byId("idfileUploader");
var domRef = fU.getFocusDomRef();
var file = domRef.files[0];
var reader = new FileReader();
var params = "EmployeesJson=";
reader.onload = function(oEvent) {
var strCSV = oEvent.target.result;
var arrCSV = strCSV.match(/[\w .]+(?=,?)/g);
var noOfCols = 6;
var headerRow = arrCSV.splice(0, noOfCols);
var data = [];
while (arrCSV.length > 0) {
var obj = {};
var row = arrCSV.splice(0, noOfCols);
for (var i = 0; i < row.length; i++) {
obj[headerRow[i]] = row[i].trim();
}
data.push(obj);
}
var Len = data.length;
data.reverse();
params += "[";
for (var j = 0; j < Len; j++) {
params += JSON.stringify(data.pop()) + ", ";
}
params = params.substring(0, params.length - 2);
params += "]";
// MessageBox.show(params);
var http = new XMLHttpRequest();
var url = oResourceBundle.getText("UploadEmployeesFile").toString();
http.onreadystatechange = function() {
if (http.readyState === 4 && http.status === 200) {
var json = JSON.parse(http.responseText);
var status = json.status.toString();
switch (status) {
case "Success":
MessageToast.show("Data is uploaded succesfully.");
break;
default:
MessageToast.show("Data was not uploaded.");
}
}
};
http.open("POST", url, true);
http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
http.send(params);
};
reader.readAsBinaryString(file);
}
});
});
After researching a little more on this issue I finally solved this issue by myself I placed a file controller and a uploader in php which return the details related to files further, we can use it to upload it on server.
Here is the code I have used.
fileUpload.html
<!DOCTYPE html>
<html><head>
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<title>Hello World</title>
<script id='sap-ui-bootstrap' src='http://localhost/resources/sap-ui-core.js' data-sap-ui-theme='sap_goldreflection'
data-sap-ui-libs='sap.ui.commons'></script>
<script>
var layout = new sap.ui.commons.layout.MatrixLayout();
layout.setLayoutFixed(false);
// create the uploader and disable the automatic upload
var oFileUploader2 = new sap.ui.commons.FileUploader("myupload",{
name: "upload2",
uploadOnChange: true,
uploadUrl: "uploader.php",
uploadComplete: function (oEvent) {
var sResponse = oEvent.getParameter("response");
if (sResponse) {
alert(sResponse);
}
}});
layout.createRow(oFileUploader2);
// create a second button to trigger the upload
var oTriggerButton = new sap.ui.commons.Button({
text:'Trigger Upload',
press:function() {
// call the upload method
oFileUploader2.upload();
$("#myupload-fu_form").submit();
alert("hi");
}
});
layout.createRow(oTriggerButton);
layout.placeAt("sample2");
</script>
</head>
<body class='sapUiBody'>
<div id="sample2"></div>
</body>
</html>
uploader.php
<?php
print_r($_FILES);
?>
It would be good if we can see your code.
This should work.
var layout = new sap.ui.commons.layout.MatrixLayout();
layout.setLayoutFixed(false);
// create the uploader and disable the automatic upload
var oFileUploader2 = new sap.ui.commons.FileUploader({
name : "upload2",
uploadOnChange : false,
uploadUrl : "../../../upload"
});
layout.createRow(oFileUploader2);
// create a second button to trigger the upload
var oTriggerButton = new sap.ui.commons.Button({
text : 'Trigger Upload',
press : function() {
// call the upload method
oFileUploader2.upload();
}
});
layout.createRow(oTriggerButton);
layout.placeAt("sample2");